Skip to content

feat(autoware): blueprint MGRS-offset fallback for sensor.other.autoware_gnss - #38

Draft
youtalk wants to merge 1 commit into
feat/ue58-demo-spawn-posefrom
feat/ue58-gnss-mgrs-fallback
Draft

feat(autoware): blueprint MGRS-offset fallback for sensor.other.autoware_gnss#38
youtalk wants to merge 1 commit into
feat/ue58-demo-spawn-posefrom
feat/ue58-gnss-mgrs-fallback

Conversation

@youtalk

@youtalk youtalk commented Sep 5, 2026

Copy link
Copy Markdown
Owner

CI status on the fork, and which job covers what. Ubicloud-PR runs two jobs and both are green. Ubuntu 24.04 / Build is the job that exercises the new gtest, and it does: it runs inside ghcr.io/youtalk/carla-ue5-toolchain:ue58-24.04, configures with cmake --preset Release -DENABLE_ROS2=ON -DBUILD_CARLA_UNREAL=OFF, builds libcarla_test_server and libcarla_test_clientLibCarla/source/test/common/*.cpp is globbed into both — and runs them. The CI log shows all three AutowareGnssOffset tests passing, including nishi_golden_offsets_after_the_mirror. But that job configures with BUILD_CARLA_UNREAL=OFF, so no fork job compiles the three Unreal plugin files this PR touches — those were compiled locally in a UE 5.8 editor build, evidenced below, and the live cells are what exercise them at runtime. Python tools and shell scripts covers exactly one further changed file: a shellcheck lint of run_carla_autoware.sh. Its pytest step covers map_tools/tests only, so the two unit tests this PR adds under PythonAPI/test/unit/ are not run by any fork job. Nothing in the CI configuration was disabled, skipped or weakened.

Description

Problem. AAutowareGnssSensor reads its MGRS offset from exactly one place: the level's AAutowareWorldSettings and the UMgrsDataAsset its soft pointer names. A level with no such asset publishes /sensing/gnss/pose_with_covariance in an unoffset frame, which on an MGRS lanelet2/PCD map puts the published pose tens of kilometres outside the map — measured below at roughly 81 km on Nishi-Shinjuku. That is not an edge case for imported content: any digital twin brought in as OpenDRIVE plus lanelet2 arrives without the asset, and a user who cannot re-save the level has no way to add one.

Why this cannot simply be fixed content-side. The obvious answer is "wire the asset into the level", and that remains the primary and preferred path — it is what the data asset is for. But the level's soft pointer is empty at load time even on a level that once had it set, because UE's RepairWorldSettings re-creates the AWorldSettings object without carrying the pointer over. Captured directly, before any wiring, in ~/ue58-logs/p3/08-summary.md: World settings (before apply): class: AutowareWorldSettings, soft_ptr: None. Re-wiring requires the editor. The runtime alternative in this PR is what makes such a map usable without one.

Change. Add three Float attributes — mgrs_offset_x, mgrs_offset_y, mgrs_offset_z, metres in the Autoware map frame — to sensor.other.autoware_gnss, and read them only when the level provides no data asset. LoadMgrsData() is restructured so that the asset stays primary: if the asset is present it is used, and a non-zero blueprint value beside it is logged and explicitly ignored. Four log lines, one per branch, make the taken path greppable in the server log. autoware_demo.py gains --mgrs_offset and run_carla_autoware.sh forwards --map-origin to it, so the value already needed by the goal conversion and by gate 1 reaches the sensor as well instead of being typed twice. CHANGELOG.md gains a matching bullet at the top of LATEST Changes — this adds three public blueprint attributes to a shipped sensor and touches Plugins/Carla/Source/**, the same class of change as the existing --ros-domain-id and Zenoh-middleware entries.

The wire values are parsed as double, deliberately. FActorAttribute stores the attribute as a string and range-validates it through As<float>(), but the value is read with FCString::Atod, so the full decimal precision survives to the sensor. This matters at MGRS magnitudes: an offset of ~1e5 m has a float quantum of about 8 mm, which is the same order as the residual the precedence probe below measures, and it would put a floor under localisation accuracy for no reason.

Two behaviours the rewrite changes that a reviewer should see stated rather than discover. First, the pre-existing MGRS Data Asset SoftPtr not set in WorldSettings. warning now also fires when the AAutowareWorldSettings cast fails, a case that was previously silent. That is a strict superset of the old condition and is an improvement — a level whose world settings are the wrong class is exactly a case worth a diagnostic — but it is a behaviour change on the log. Second, the rewrite silently closes a latent bug that no test covered: the old code assigned MgrsDataAsset unconditionally after LoadSynchronous(), so a broken soft reference left the pointer null with no diagnostic at all. The new flow falls through to the warning.

Where has this been tested?

  • Platform(s): Ubuntu 24.04, x86_64
  • Python version(s): 3.12 (host client), 3.10/3.12 inside the Autoware containers
  • Unreal Engine version(s): UE 5.8, CARLA fork CarlaUnreal/UnrealEngine@ue58-dev-carla cacb25b99f14. The three plugin files were compiled in a local UE 5.8 editor build with zero error: lines and no diagnostic naming any of them; no fork CI job compiles them.

Before, on the pristine level (~/ue58-logs/p3/03-gnss-before.txt, and ~/ue58-logs/p3/02-server-grep.txt line 3 — that file is three lines long, and the 4763: it carries inside is a grep -n prefix from the simulator log the grep was taken over, which later probes have since overwritten):

Before
published /sensing/gnss/pose_with_covariance x=-279.058 y=-220.875 z=0.545
expected without the MGRS offset x=-279.059 y=-220.875 z=0.545matches
expected with the MGRS offset x=81376.671 y=49916.555 z=43.045 — no match
MGRS Data Asset SoftPtr not set in WorldSettings. in the server log present
gates never run in this state — see the note below

The "before" column is one topic sample plus one log line, not a failed run, and must not be read as if the gates had been measured with the offset absent. What was measured is the published pose and the server-log warning. No cell in this work ever ran the gates in that state: the fallback cell is a pristine level with the fallback supplying the offset, and the other two cells are wired. That NDT would fail to initialise roughly 81 km outside the point-cloud map is a reasoned expectation from the measured displacement, not a result; measuring it would need a further cell with the offset deliberately suppressed, which was not run.

After — the same map in both level states, each driving a full closed loop to ARRIVED. Both cells used identical map, route, poses, build and harness; the only difference is which source supplies the offset.

Cell level state offset source ROS distro G1 NDT max G2 closest G3 LiDAR routing state
N-content (~/ue58-logs/p3-10-cell-nishi-content/) wired level data asset Jazzy 0.198 m 0.081 m 10.00 Hz 3 = ARRIVED
N-fallback (~/ue58-logs/p3-11-cell-nishi-fallback/) pristine blueprint fallback Jazzy 0.139 m 0.075 m 10.00 Hz 3 = ARRIVED
N-humble (~/ue58-logs/p3-12-cell-nishi-humble/) wired level data asset Humble 0.297 m 0.075 m 10.00 Hz 3 = ARRIVED

Which branch ran, per cell — one gnss-source.txt per cell directory, corroborated in both carla_server.log and the cell's copy of the engine log at the same timestamp and frame. With the level wired (N-content), two lines at the identical timestamp:

LogCarla: AutowareGnssSensor: MGRS offset from level data asset (81655.730000, 50137.430000, 42.499980)
LogCarla: Warning: AutowareGnssSensor: level data asset present; ignoring blueprint mgrs_offset (81655.730000, 50137.430000, 42.499980)

With the level pristine (N-fallback):

LogCarla: AutowareGnssSensor: no MGRS data asset in WorldSettings; using blueprint mgrs_offset fallback (81655.730000, 50137.430000, 42.499980)

In the two wired cells MGRS Data Asset SoftPtr not set appears 0 times; in the fallback cell so do both data-asset lines, and the pre-existing warning is correctly suppressed rather than fired alongside a working offset. N-humble reproduces the asset path to six decimals.

The offset reaches Autoware's map frame, not merely a log line. The engage precheck compares CARLA ground truth against Autoware's own /localization/kinematic_state belief, and prints the same result in all three cells (…/automation.log): GATE1: truth (81376.10, 49916.10) yaw 34.0 deg | belief (81376.13, 49916.12) yaw 34.0 deg | delta 0.04 m, 0.0 deg. The belief side is computed entirely inside the stack, so this is an origin-independent second witness, and it is the one that shows propagation rather than branch selection. The two kinds of evidence are complementary and both belong here: the precheck cannot say which source supplied the offset, and the grep cannot say the value propagated. The agreement is identity at the printed precision of 2 decimal places — 1 cm — so it is not bitwise identity.

Precedence under disagreement, proved behaviourally. In the two gate cells the fallback carried a value identical to the asset's, so those cells cannot distinguish "the asset won" from "either source would have produced the same number". A dedicated probe closes that gap (~/ue58-logs/p3-10b-precedence/): wired level, plus a deliberately divergent --mgrs_offset=1000,2000,3000, about 94 km away.

Quantity Value
published GNSS pose (81375.952382, 49916.583549, 43.035453)
candidate from the asset (81375.952382, 49916.586601, 43.035328)
candidate from the blueprint (720.222382, 1779.156601, 3000.535348)
distance to the asset candidate 0.003054 m
distance to the blueprint candidate 93975.026 m

The blueprint value was genuinely supplied — mgrs_offset_x/y/z has_attribute = True on the live server (…/blueprint-attrs.txt) — and 1000/2000/3000 appear nowhere in the level. Both log lines now carry different numbers, which is what makes the "ignoring blueprint mgrs_offset" line load-bearing instead of echoing the asset's own values as it did in the gate cells. The ~3 mm residual is a tick-pairing artefact rather than offset error: the ROS publish and the MGRS arithmetic use the same SensorWorldTransform captured within one tick, and the residual is the external client's get_transform() landing on a later tick while the ego micro-settles; the later of the two paired samples has the smaller residual (3.05 mm against 10.58 mm). Sensor noise is ruled out independently — noise_*_stddev and *_bias all default to 0.0f, the demo never sets them, and the published covariance is all zeros.

The claim that evidence licenses, scoped. Verified on Nishi-Shinjuku with sensor.other.autoware_gnss given a deliberately divergent blueprint fallback (1000/2000/3000 m, ~94 km away): the server log confirms the data-asset offset was read and the blueprint offset explicitly ignored, and the published /sensing/gnss/pose_with_covariance matches the asset candidate to ~3 mm while sitting ~94 km from the blueprint candidate. AAutowareGnssSensor::LoadMgrsData() is a pure presence check — if (MgrsDataAsset) { …; return; } followed by if (bHasMgrsOffsetFallback) — with no magnitude or sign comparison anywhere, so the fact that only one direction of disagreement was measured is a weaker limitation than n = 1 usually implies: no code path exists in which the relative size or sign of the two offsets could change which branch wins. A bare universal "the asset always takes precedence" is not asserted.

Unit and gtest coverage, run locally. libcarla_test_server and libcarla_test_client each report 10 tests from 2 suites, 10 passed — 3 new AutowareGnssOffset tests plus the 7 pre-existing TransformQuaternion tests (~/ue58-logs/p3/13-gtest.txt). Read that coverage honestly: the new test reproduces AutowareGNSSPublisher::Write's composition rather than invoking it, so a reordering inside Write() would leave it green; what it does pin is the TransformQuaternion.h Y-mirror and the Nishi golden offsets that every consumer of this offset shares. The test file says so in its own header comment. Two Python tests for --mgrs_offset parsing were added under PythonAPI/test/unit/ and pass locally; no fork job runs that directory.

The new C++ is provably live in the binary that ran the cells, which is the trap this project has hit before — a green build can silently run a stale editor module. nm -DC …/Plugins/Carla/Binaries/Linux/libUnrealEditor-Carla.so | grep -c SetMgrsOffsetFallback returns 1, and the module .so is timestamped inside that build's window (~/ue58-logs/p3/13-build-editor.log). Use nm -DC: plain nm -C reports "no symbols" on this .so.

Limits of the live evidence, stated plainly. n = 1 per arm: one map, one offset value, one build, Fast DDS only; the ROS distro is the only axis with two samples. Nothing here says anything about cruise-phase localisation — the gate harness runs G2, then a settle, then G3, then G1, so every G1 window in this work opens about 52 s into a ~62 s route and covers the terminal ~31 m of a 221 m drive at around 1 m/s mean. Those windows are genuine in-motion data from an unmodified harness, but each cell's G1 maximum is a transient two-sample spike, not a tracking-quality measure, and the same configuration produced 0.055 m and 0.198 m in two runs differing only in where the window was placed — a same-config spread wider than the 0.059 m difference between the asset and fallback cells. The correct reading of that comparison is that the fallback is not a regression on the asset path, not that the two are equivalent to within a few centimetres.

Possible Drawbacks

  • Two sources of truth for one number. A map can now be georeferenced by its level asset or by a blueprint attribute, and a user who sets both and expects the blueprint to win will be surprised. This is mitigated rather than removed: the asset is unconditionally primary, and a non-zero blueprint value supplied beside it is reported at Warning level with both values printed, so the situation is visible in the server log rather than silent. The alternative — letting the blueprint override — would let a client silently move a level's georeference, which is worse.
  • A deliberate mgrs_offset of 0,0,0 is indistinguishable from "not supplied". SetMgrsOffsetFallback sets bHasMgrsOffsetFallback = !OffsetMeters.IsNearlyZero(), so an explicit all-zero fallback emits neither log line and falls through to the pre-existing warning. On a Local-projector map a zero offset is also the correct behaviour, so this is arguably right, but it is untested by any cell and it is a diagnostics wart: the offset applied is {0,0,0} either way, so no published output differs.
  • Nothing cross-checks the fallback against the map. A wrong mgrs_offset produces a confidently wrong pose, and the symptom is a localisation failure rather than a configuration error. The same is true of the --map-origin value earlier in this series; forwarding one to the other at least means the value is typed once.
  • The "Town10HD is unchanged" guarantee in the run script is textual. The forwarding guard is a string comparison against 0,0,0, so --map-origin "0.0,0.0,0.0" would be forwarded rather than suppressed. Harmless — the server maps a zero fallback back to "not supplied" — but the guarantee is spelling-based, not numeric.
  • --mgrs_o is no longer an unambiguous abbreviation of the pre-existing --mgrs_off, now that --mgrs_offset exists, and the two flags read as near-opposites: one disables MGRS handling, the other supplies the offset. Renaming either is a wider change than this PR should carry, but the collision is worth knowing about.
  • The plugin change is compiled by no job in the fork's CI. The fork's build job configures with BUILD_CARLA_UNREAL=OFF, so the only compile evidence for AutowareGnssSensor.{h,cpp} and ActorBlueprintFunctionLibrary.cpp is the local UE 5.8 editor build cited above. The upstream self-hosted workflow does build the Unreal project and would cover them, but it does not run on this fork.

…are_gnss

The Autoware GNSS sensor reads its MGRS offset only from the level's
AutowareWorldSettings -> UMgrsDataAsset. A level that has no such
asset -- any imported digital twin, and every map a user cannot
re-save -- publishes /sensing/gnss/pose in an unoffset frame, so on an
MGRS lanelet2/PCD map Autoware's initial pose lands tens of kilometres
off the map, where NDT cannot be expected to initialise. UE's
RepairWorldSettings gives such a level an AutowareWorldSettings with an
empty pointer at load time, so the content path cannot be reached
without the editor.

Add mgrs_offset_x/y/z (Float, metres, Autoware map frame) to
sensor.other.autoware_gnss, read only when the level provides no data
asset; the asset stays primary and a non-zero fallback beside it is
logged and ignored. Parse the wire values as double: an offset of
~1e5 m would resolve to ~8 mm as float. autoware_demo.py gains
--mgrs_offset and run_carla_autoware.sh forwards --map-origin to it. A
gtest pins the CARLA->ROS Y mirror and the Nishi golden offsets shared
by every consumer of this offset.

Signed-off-by: Yutaka Kondo <yutaka.kondo@youtalk.jp>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant