|
1 | 1 | [Draft for Discourse] |
2 | 2 |
|
3 | | -# Layered Global Occupancy Map |
| 3 | +# Layered Global Map Observations |
4 | 4 |
|
5 | | -This post describes the first implemented slice of a layered global occupancy map for the next generation Open-RMF prototype. |
| 5 | +This post describes the observation interface for contributing temporary map |
| 6 | +information to the next generation Open-RMF prototype. |
6 | 7 |
|
7 | | -The goal of this slice is to let temporary local observations affect the global planning map without changing the path server interface. The path server can continue to consume a normal `nav_msgs/OccupancyGrid` from `/map`, while a new map server composes that grid from a static map and active observation layers. |
| 8 | +The goal is to let robots and perception systems publish what they currently |
| 9 | +observe without tying them to a specific central map implementation. The first |
| 10 | +prototype consumes sparse 2D occupancy regions because that is enough for the |
| 11 | +current global planning grid, but the interface is intentionally isolated in |
| 12 | +`rmf_layered_map_msgs` so richer representations, such as height-aware or voxel |
| 13 | +observations, can be added later. |
8 | 14 |
|
9 | 15 | # Quick Summary |
10 | 16 |
|
11 | | -* Robots and perception sources can publish sparse 2D region updates with a time-to-live (TTL) |
12 | | -* A central layered map server composes a static map with active dynamic observations |
13 | | -* Observations from multiple sources are stitched into one composed global grid |
14 | | -* The composed output is published as `nav_msgs/OccupancyGrid` on `/map` |
15 | | -* The observation messages live in `rmf_layered_map_msgs`, leaving `rmf_prototype_msgs` unchanged |
16 | | -* The first implementation is a Rust package named `rmf_layered_map_server` under `map_server/` |
| 17 | +* Observation sources publish sparse temporary map patches |
| 18 | +* One message can contain both clear-space and occupied-space patches from the |
| 19 | + same sensor snapshot |
| 20 | +* Clear-space patches have TTLs, just like obstacle patches |
| 21 | +* A source can reset its previous observations without using a TTL |
| 22 | +* Robot-mounted sources can include the robot pose at observation time |
| 23 | +* The observation messages live in `rmf_layered_map_msgs`, leaving |
| 24 | + `rmf_prototype_msgs` unchanged |
17 | 25 |
|
18 | | -# Background |
| 26 | +# Observation Topic |
19 | 27 |
|
20 | | -The static map support in the prototype gives the path server a useful baseline: it already consumes a `nav_msgs/OccupancyGrid` from `/map` and can plan around occupied cells. This implementation preserves that contract. Instead of teaching the planner about every possible observation source, the layered map server publishes one composed `/map`. |
| 28 | +Observation sources publish dynamic map observations on: |
21 | 29 |
|
22 | | -Local observations from robots are different from static map data. They may describe temporary objects such as pedestrians, carts, doors held open, or equipment left in the path. For that reason the central map treats robot observations as dynamic layers that expire unless they are refreshed. |
| 30 | +* `/map/region_updates` - [`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) |
23 | 31 |
|
24 | | -This implementation builds on the existing static map behavior. |
| 32 | +The topic is an event stream. Updates are not latched because expired |
| 33 | +observations should not be replayed to a restarted map service as if they were |
| 34 | +current. |
25 | 35 |
|
26 | | -# Interface Topology Overview |
27 | | - |
28 | | -```mermaid |
29 | | -flowchart LR |
30 | | - static["Static map server"] -- "/map/static" --> layered["Layered map server"] |
31 | | - observer["Observation source"] -- "/map/region_updates" --> layered |
32 | | - layered -- "composed /map" --> path["Path server"] |
33 | | -``` |
34 | | - |
35 | | -The layered map server owns the composed `/map` topic. When it is present, any static map publisher should be remapped to `/map/static`. |
36 | | - |
37 | | -The path server continues to subscribe to `/map`. It does not subscribe directly to local robot observations. This keeps map composition independent from multi-agent planning and allows alternative map server implementations to be swapped in later. |
38 | | - |
39 | | -Each observation stream publishes updates with a unique `source_id`, such as `robot_1/local_costmap` or `robot_2/lidar_obstacles`. The layered map server tracks these sources independently but composes their active observations into a single `OccupancyGrid`. |
40 | | - |
41 | | -# Observation Topics |
42 | | - |
43 | | -* `/map/region_updates` - [`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg): Dynamic local observation updates |
44 | | - |
45 | | -The topic is treated as an event stream. Updates are not latched because expired observations should not be replayed to a restarted map server as if they were current. |
| 36 | +Each observation stream should use a stable `source_id`, such as |
| 37 | +`robot_1/local_costmap`, `robot_1/front_lidar`, or `door_sensor/west_lobby`. |
| 38 | +The map service tracks each source independently so one robot can update or |
| 39 | +reset its own temporary observations without disturbing observations from other |
| 40 | +sources. |
46 | 41 |
|
47 | 42 | # Messages |
48 | 43 |
|
49 | 44 | ## `MapObservationSource` |
50 | 45 |
|
51 | | -[`MapObservationSource.msg`](../rmf_layered_map_msgs/msg/MapObservationSource.msg) describes where an observation came from. |
| 46 | +[`MapObservationSource.msg`](../rmf_layered_map_msgs/msg/MapObservationSource.msg) |
| 47 | +describes where an observation came from. |
52 | 48 |
|
53 | 49 | Important fields: |
54 | 50 |
|
55 | | -* `source_id`: stable source identifier, usually the robot namespace plus the local source name |
56 | | -* `robot_name`: robot that produced the observation, if the source is mounted on a robot. This can be empty for fixed sensors or synthetic layers |
57 | | -* `map_name`: map or level that the observation belongs to |
58 | | -* `frame_id`: coordinate frame for the regions |
59 | | -* `stamp`: time when the observation snapshot was produced |
60 | | -* `default_ttl`: fallback TTL for updates from this source |
| 51 | +* `header`: frame and timestamp for the observed regions |
| 52 | +* `source_id`: stable source identifier, usually the robot namespace plus the |
| 53 | + local source name |
| 54 | +* `robot_name`: robot that produced this observation, if the source is mounted |
| 55 | + on a robot. This can be empty for fixed sensors or synthetic map layers |
| 56 | +* `map_name`: map or level that the observations belong to |
| 57 | +* `robot_pose`: robot pose when the observation was produced, so consumers do |
| 58 | + not need to reconstruct this from a separate TF-like lookup |
| 59 | +* `default_ttl_sec`: fallback TTL in seconds for patches from this source |
61 | 60 |
|
62 | 61 | ## `MapRegionUpdate` |
63 | 62 |
|
64 | | -[`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) describes one sparse update from a local observation source. |
| 63 | +[`MapRegionUpdate.msg`](../rmf_layered_map_msgs/msg/MapRegionUpdate.msg) |
| 64 | +describes one observation snapshot from a source. |
65 | 65 |
|
66 | | -Update types: |
67 | | - |
68 | | -* `UPDATE_OBSTACLE`: regions are temporary occupied space |
69 | | -* `UPDATE_CLEAR`: regions are temporary free space |
70 | | -* `UPDATE_RESET`: previous observations from the same source should be removed |
71 | | - |
72 | | -The first implementation uses `rmf_prototype_msgs/Region` for 2D geometry. This keeps the message sparse and lets the central map server rasterize regions into its composed `OccupancyGrid`. |
73 | | - |
74 | | -The map observation messages live in a dedicated `rmf_layered_map_msgs` package. This keeps `rmf_prototype_msgs` as a stable public API surface while allowing the layered-map interface to evolve in its own package. |
| 66 | +Important fields: |
75 | 67 |
|
76 | | -# Layered Map Server |
| 68 | +* `source`: metadata for the observation source |
| 69 | +* `reset_source`: remove active observations from the same `source_id` and |
| 70 | + `map_name` before applying the new patches |
| 71 | +* `patches`: ordered clear-space and occupied-space patches from this snapshot |
77 | 72 |
|
78 | | -The first server implementation is a Rust package named `rmf_layered_map_server` under the `map_server/` directory. |
| 73 | +`reset_source` is useful when a source publishes replacement snapshots, changes |
| 74 | +maps, shuts down, or knows its previous observation state is no longer valid. It |
| 75 | +does not have a TTL because it is a bookkeeping operation, not an active map |
| 76 | +observation. |
79 | 77 |
|
80 | | -Inputs: |
| 78 | +## `MapRegionPatch` |
81 | 79 |
|
82 | | -* `/map/static`: `nav_msgs/OccupancyGrid`, transient local, reliable |
83 | | -* `/map/region_updates`: `rmf_layered_map_msgs/MapRegionUpdate`, reliable |
| 80 | +[`MapRegionPatch.msg`](../rmf_layered_map_msgs/msg/MapRegionPatch.msg) |
| 81 | +describes one group of regions with the same action and TTL. |
84 | 82 |
|
85 | | -Output: |
| 83 | +Patch types: |
86 | 84 |
|
87 | | -* `/map`: composed `nav_msgs/OccupancyGrid`, transient local, reliable |
| 85 | +* `UPDATE_CLEAR`: regions are temporary free space |
| 86 | +* `UPDATE_OBSTACLE`: regions are temporary occupied space |
88 | 87 |
|
89 | | -The server keeps the static grid separate from active dynamic observations: |
| 88 | +Clear patches and obstacle patches both require a TTL because both describe |
| 89 | +temporary observations. If a patch TTL is zero or negative, the source |
| 90 | +`default_ttl_sec` is used. If that is also zero or negative, the map service's |
| 91 | +configured default TTL is used. |
90 | 92 |
|
91 | | -```text |
92 | | -LayeredMap |
93 | | - static_grid: OccupancyGrid |
94 | | - dynamic_observations: Vec<DynamicObservation> |
95 | | - revision: u64 |
| 93 | +When a sensor snapshot includes both clear and occupied regions, publish them in |
| 94 | +one `MapRegionUpdate` message. The map service applies clear patches before |
| 95 | +obstacle patches, which gives deterministic "clear then mark" behavior without |
| 96 | +relying on the arrival order of separate ROS messages. |
96 | 97 |
|
97 | | -DynamicObservation |
98 | | - source_id: String |
99 | | - map_name: String |
100 | | - update_type: obstacle | clear |
101 | | - occupancy_value: i8 |
102 | | - regions: Vec<Region> |
103 | | - expires_at: Time |
104 | | -``` |
| 98 | +The first prototype uses `rmf_prototype_msgs/Region` for sparse 2D geometry so |
| 99 | +robots can publish compact patches. The central map service is responsible for |
| 100 | +rasterizing those regions into its internal representation. |
105 | 101 |
|
106 | | -Composition rules: |
| 102 | +# Example Flow |
107 | 103 |
|
108 | | -1. Start from the latest static grid. |
109 | | -2. Drop expired dynamic observations before composing. |
110 | | -3. Rasterize active clear regions to free space. |
111 | | -4. Rasterize active obstacle regions to occupied space. |
112 | | -5. Active obstacle observations win over active clear observations for the same cell. |
113 | | -6. Unknown static cells stay unknown unless an active clear or obstacle observation covers them. |
| 104 | +A local costmap or LiDAR observation node can publish a replacement snapshot by: |
114 | 105 |
|
115 | | -`UPDATE_RESET` removes observations from the same source and map without disturbing observations from other robots. For example, if `robot_1/local_costmap` resets its obstacle layer, active observations from `robot_2/local_costmap` remain in the composed grid. |
| 106 | +1. Stamping the observation source with the sensor frame and observation time. |
| 107 | +2. Setting `reset_source` if the new snapshot replaces the source's previous |
| 108 | + temporary observations. |
| 109 | +3. Adding one or more clear patches for free space seen by the sensor. |
| 110 | +4. Adding one or more obstacle patches for occupied space seen by the sensor. |
| 111 | +5. Giving each patch a TTL long enough to survive normal publication jitter but |
| 112 | + short enough to decay when the observation is no longer refreshed. |
116 | 113 |
|
117 | | -The server does not use the full `-/errors` component pattern from the broader topic taxonomy. For transient observations, TTL expiration is the primary recovery mechanism. |
| 114 | +The map service should ignore snapshots from a source if their timestamp is |
| 115 | +older than a newer snapshot that has already been accepted. This prevents a late |
| 116 | +clear/reset message from removing obstacle information that came from a newer |
| 117 | +observation. |
118 | 118 |
|
119 | 119 | # Implemented Test Coverage |
120 | 120 |
|
121 | | -The Rust tests cover: |
| 121 | +The first server tests cover: |
122 | 122 |
|
123 | | -* composing obstacle regions over a static map |
| 123 | +* composing obstacle regions over a static planning grid |
124 | 124 | * point regions with non-zero map origins and non-1.0 resolutions |
125 | 125 | * polygon regions, out-of-bounds regions, and malformed region point arrays |
126 | 126 | * pruning expired observations by TTL |
127 | | -* active obstacle observations winning over active clear observations |
| 127 | +* clear and obstacle patches in the same update |
| 128 | +* late older snapshots being ignored |
128 | 129 | * reset updates removing observations from the same source and map |
129 | 130 | * multiple robot sources being stitched into one composed grid |
130 | | - |
131 | | -The `rmf_layered_map_server_demo` package includes a small publisher that sends a static map and a temporary obstacle update. Its launch file starts the layered map server and RViz on `/map`, so developers can see the dynamic layer appear and decay without needing the full Nav2 or replanning loop. |
132 | | - |
133 | | -The `rmf_layered_map_server_test` package adds a launch test for the ROS topic contract: `/map/static` and `/map/region_updates` are consumed, `/map` is published, reset updates remove active observations, and obstacles are removed after TTL expiry. |
0 commit comments