SvgMapConverter reads x/y/d straight off the SVG attributes and never looks at an ancestor transform. Every classic_bottleneck* map wraps its whole pedestrian group in transform="translate(0,-4.3651647)", so the loaded pedestrian geometry sits 4.3651647 m from where the map file draws it — and the loaded pedestrian goal zone ends up partly inside the northern boundary wall and partly outside the viewBox.
Verified against origin/main 612de6f45 and against e2ac534c9d6bb750346b1e0724638c91306e410a (the commit a downstream figure pins). grep -c transform robot_sf/nav/svg_map_parser.py returns 0 at both.
Where it comes from
_get_svg_info collects elements with self.svg_root.findall(".//svg:rect") (and the same for path / circle), which is a descendant-or-self search that discards the parent chain. _parse_rect_element then does float(rect.attrib.get("x")), float(rect.attrib.get("y")); _parse_path_element reads d directly. Nothing between the XML and the MapDefinition consults transform.
There is no converted-map artifact in between either: each scenario's map_file names the .svg, and SvgMapConverter parses it at run time.
Affected maps
maps/svg_maps/classic_bottleneck.svg, classic_bottleneck_medium.svg, classic_bottleneck_high.svg — all three carry exactly one transform, on the group holding ped_spawn_zone, ped_goal_zone and ped_route_0_0:
transform="translate(0,-4.3651647)"
The robot group, the obstacles and the single_ped_* markers carry none, so only the pedestrian elements move.
Measured consequence (low-tier map, all three identical in drawn geometry)
| element |
as the map file draws it |
as SvgMapConverter loads it |
ped_spawn_zone |
y 1.635 – 5.635 |
y 6.000 – 10.000 |
ped_goal_zone |
y 33.770 – 37.770 |
y 38.135 – 42.135 |
ped_route_0_0 |
(20, 8.929) → (20, 23.635) |
(20, 13.294) → (20, 28.000) |
| robot/pedestrian route crossing |
(20.00, 16.10) |
(20.00, 24.56) |
Uniform shift (0, −4.3651647) m; the routes' crossing point — where the designed encounter happens — moves 8.4662 m.
The loaded ped_goal_zone is the part that looks like a defect rather than a preference:
- its
viewBox is 0 0 40 40, and the loaded zone spans y 38.135 – 42.135, so 2.135 m of its 4.00 m depth is outside the map;
- obstacle
rect2 is the northern boundary wall at y 39 – 40 across the full width, and the loaded zone overlaps it by 4.00 m² (1.00 m deep over the zone's 4.00 m width) — a quarter of the goal zone is inside a wall.
A goal zone that is partly outside the map and partly inside a wall is not a plausible authoring intent, which is what points at the loader rather than at the maps.
Why this is being reported from outside
A dissertation figure documents the bottleneck archetype's geometry (ll7/diss#2127, ll7/diss#2136). It draws the maps as this loader reads them, because that is the geometry the benchmark campaign ran — but it has to disclose in its caption that the drawing diverges from the map file, which is only correct for as long as this behaviour stands. If the loader is changed to apply transforms, that figure's disclosure and any campaign result that depended on the pedestrian placement both need revisiting.
Possible dispositions
- Apply ancestor transforms in
SvgMapConverter (walk depth-first accumulating translate, refuse anything that is not a translate rather than ignoring it). This changes the loaded geometry of the three bottleneck maps, so it is behaviour-changing for any existing result on them.
- Reject maps that carry transforms, so an authored transform is a loud error instead of a silent 4.37 m displacement.
- Flatten the transform in the three map files (bake the offset into the child coordinates) and keep the loader as it is. This also changes the loaded geometry.
- Document the current behaviour as the contract and treat the map files as needing to be authored transform-free.
(1) or (2) removes the silent-divergence class; (3) fixes these three maps without closing the class; (4) closes nothing but at least makes the contract explicit. Whichever is chosen, the ped_goal_zone-inside-the-wall geometry deserves a look on its own.
Reproduction
import xml.etree.ElementTree as ET
NS = {"svg": "http://www.w3.org/2000/svg"}
LBL = "{http://www.inkscape.org/namespaces/inkscape}label"
root = ET.parse("maps/svg_maps/classic_bottleneck.svg").getroot()
# what the loader sees
for r in root.findall(".//svg:rect", NS):
if r.attrib.get(LBL) == "ped_goal_zone":
print(r.attrib["x"], r.attrib["y"]) # 17.801584 38.135078
# what the file draws
for g in root.iter():
if g.attrib.get("transform"):
print(g.attrib["transform"]) # translate(0,-4.3651647)
Ruled implementation contract — 2026-09-03
Objective
Implement ruling option 1: make authored ancestor translate(...) transforms effective in
SvgMapConverter, reject unsupported transform classes, and preserve exact historical/as-run
geometry through an explicit compatibility version rather than silently reinterpreting old
evidence.
Inputs
- Parser owner:
robot_sf/nav/svg_map_parser.py at the implementation branch's exact current-main
base.
- Affected fixtures: all repository SVG maps carrying ancestor transforms, including the three
maps/svg_maps/classic_bottleneck*.svg maps named above.
- Historical boundary: pinned campaigns and figures that used the legacy transform-ignoring parser.
- Related truth boundary:
ll7/diss#2144 establishes that the low-tier bottleneck cell authored no
pedestrian markers; transform support must not be described as adding pedestrians to that cell.
Scope
In scope:
- accumulate nested translations for parsed paths, rectangles, and circles;
- fail closed on
scale, rotate, skew, matrix, malformed transforms, and unsupported mixed
transform lists;
- add an explicit legacy/corrected parser or map-geometry contract selector and bind it into
reproducible configuration/manifests;
- add analytic parser tests and map-level bottleneck geometry assertions;
- document the historical-results and corrected-execution compatibility boundary.
Out of scope:
- rerunning a benchmark campaign in this implementation slice;
- rewriting, deleting, or reclassifying frozen artifacts or figures;
- adding missing pedestrian markers or changing scenario density;
- planner-performance, safety, or paper-facing claims.
Acceptance criteria
Verification
- Add red/green focused parser tests for nested translations and each rejected transform class.
- Add exact-coordinate tests for all transformed repository maps and the three bottleneck maps.
- Run the focused navigation/map parser test modules, Ruff, format check, and
git diff --check.
- Run the full repository PR-readiness path because map geometry and benchmark comparability are
affected.
- Obtain independent domain-aware review of the exact head before merge; implementation proof is
not benchmark or paper-grade evidence.
Decision and claim boundary
Ruling token: 1. Confidence: 0.98 that silent transform omission is a loader defect; 0.92 that
bounded translate support with explicit legacy reproduction is the best correction. No campaign,
artifact replacement, historical-result reinterpretation, safety claim, or paper-facing claim is
authorized by this contract.
SvgMapConverterreadsx/y/dstraight off the SVG attributes and never looks at an ancestortransform. Everyclassic_bottleneck*map wraps its whole pedestrian group intransform="translate(0,-4.3651647)", so the loaded pedestrian geometry sits 4.3651647 m from where the map file draws it — and the loaded pedestrian goal zone ends up partly inside the northern boundary wall and partly outside theviewBox.Verified against
origin/main612de6f45and againste2ac534c9d6bb750346b1e0724638c91306e410a(the commit a downstream figure pins).grep -c transform robot_sf/nav/svg_map_parser.pyreturns 0 at both.Where it comes from
_get_svg_infocollects elements withself.svg_root.findall(".//svg:rect")(and the same forpath/circle), which is a descendant-or-self search that discards the parent chain._parse_rect_elementthen doesfloat(rect.attrib.get("x")),float(rect.attrib.get("y"));_parse_path_elementreadsddirectly. Nothing between the XML and theMapDefinitionconsultstransform.There is no converted-map artifact in between either: each scenario's
map_filenames the.svg, andSvgMapConverterparses it at run time.Affected maps
maps/svg_maps/classic_bottleneck.svg,classic_bottleneck_medium.svg,classic_bottleneck_high.svg— all three carry exactly one transform, on the group holdingped_spawn_zone,ped_goal_zoneandped_route_0_0:The robot group, the obstacles and the
single_ped_*markers carry none, so only the pedestrian elements move.Measured consequence (low-tier map, all three identical in drawn geometry)
SvgMapConverterloads itped_spawn_zoneped_goal_zoneped_route_0_0Uniform shift (0, −4.3651647) m; the routes' crossing point — where the designed encounter happens — moves 8.4662 m.
The loaded
ped_goal_zoneis the part that looks like a defect rather than a preference:viewBoxis0 0 40 40, and the loaded zone spans y 38.135 – 42.135, so 2.135 m of its 4.00 m depth is outside the map;rect2is the northern boundary wall at y 39 – 40 across the full width, and the loaded zone overlaps it by 4.00 m² (1.00 m deep over the zone's 4.00 m width) — a quarter of the goal zone is inside a wall.A goal zone that is partly outside the map and partly inside a wall is not a plausible authoring intent, which is what points at the loader rather than at the maps.
Why this is being reported from outside
A dissertation figure documents the bottleneck archetype's geometry (ll7/diss#2127, ll7/diss#2136). It draws the maps as this loader reads them, because that is the geometry the benchmark campaign ran — but it has to disclose in its caption that the drawing diverges from the map file, which is only correct for as long as this behaviour stands. If the loader is changed to apply transforms, that figure's disclosure and any campaign result that depended on the pedestrian placement both need revisiting.
Possible dispositions
SvgMapConverter(walk depth-first accumulatingtranslate, refuse anything that is not a translate rather than ignoring it). This changes the loaded geometry of the three bottleneck maps, so it is behaviour-changing for any existing result on them.(1) or (2) removes the silent-divergence class; (3) fixes these three maps without closing the class; (4) closes nothing but at least makes the contract explicit. Whichever is chosen, the
ped_goal_zone-inside-the-wall geometry deserves a look on its own.Reproduction
Ruled implementation contract — 2026-09-03
Objective
Implement ruling option 1: make authored ancestor
translate(...)transforms effective inSvgMapConverter, reject unsupported transform classes, and preserve exact historical/as-rungeometry through an explicit compatibility version rather than silently reinterpreting old
evidence.
Inputs
robot_sf/nav/svg_map_parser.pyat the implementation branch's exact current-mainbase.
maps/svg_maps/classic_bottleneck*.svgmaps named above.ll7/diss#2144establishes that the low-tier bottleneck cell authored nopedestrian markers; transform support must not be described as adding pedestrians to that cell.
Scope
In scope:
scale,rotate,skew,matrix, malformed transforms, and unsupported mixedtransform lists;
reproducible configuration/manifests;
Out of scope:
Acceptance criteria
circles.
current spawn/robot-goal and pedestrian-goal/wall overlaps.
pooled accidentally.
Verification
git diff --check.affected.
not benchmark or paper-grade evidence.
Decision and claim boundary
Ruling token:
1. Confidence: 0.98 that silent transform omission is a loader defect; 0.92 thatbounded translate support with explicit legacy reproduction is the best correction. No campaign,
artifact replacement, historical-result reinterpretation, safety claim, or paper-facing claim is
authorized by this contract.