Skip to content

fix(data_augmentation): quintic kinematic consistency - #348

Draft
rej55 wants to merge 6 commits into
tier4-mainfrom
fix/quintic-kinematic-consistency-main
Draft

fix(data_augmentation): quintic kinematic consistency#348
rej55 wants to merge 6 commits into
tier4-mainfrom
fix/quintic-kinematic-consistency-main

Conversation

@rej55

@rej55 rej55 commented Jul 31, 2026

Copy link
Copy Markdown

What

Fixes five kinematic inconsistencies in the quintic augmentation path
(StatePerturbation in utils/data_augmentation.py), one per commit:

# commit issue
1 use the current heading for the quintic start tangent theta0 came from the chord to the GT point 1 s ahead, which lags the tangent and ignores the heading perturbation
2 use signed tangential terms in the quintic boundary conditions v0/a0/vT/aT were vector magnitudes: sign dropped, and the centripetal term double-counted
3 keep ay consistent with the perturbed speed ay = vx * yaw_rate broke by dvx * yaw_rate when vx was perturbed
4 stop the heading perturbation from injecting sideslip the velocity vector kept pointing along the old heading, i.e. a sideslip equal to the heading perturbation
5 scale the past before building the interpolated future the ego_past_noise_std scaling was applied after the target had already been built from the unscaled state

augment_type defaults to "quintic", so this is the path actually used in training.

Why this matters

interpolation_future_trajectory() was not the identity even with zero
perturbation
. Feeding it a synthetic GT plus a current state that exactly matches
that GT should return the GT unchanged; instead it rewrote the first 2 s by up to
1.1 m. So the augmentation was corrupting the training target for accelerating,
braking and turning samples independently of the perturbation itself — every sample
with augment_prob hit and |vx| >= 2 m/s.

The two root causes were both in how the boundary conditions were built:

  • torch.norm() on (ax, ay) dropped the sign, so a braking sample was handed a
    positive longitudinal acceleration.
  • The same norm folded the lateral component in. ay is the centripetal term
    v * omega, and the quintic already expresses that through its
    -v0*sin(theta0)*omega0 term, so it was counted twice. On a constant-speed curve
    the true tangential acceleration is exactly 0 while ||(ax, ay)|| = v^2 * kappa
    — 3.84 m/s² at 8 m/s on a 0.06 1/m curve.

Verification

Zero-perturbation identity

Synthetic GT with a current state consistent with it, so the interpolation must be
the identity. Max position error over the bridged 0–2 s window:

case before after
8 m/s straight, a=0 0.000 m 0.000 m
8 m/s straight, a=−2 (braking) 0.553 m 0.031 m
8 m/s, kappa=0.03 constant-speed turn 0.533 m 0.030 m
8 m/s, kappa=0.06 constant-speed turn 1.102 m 0.065 m
3 m/s, kappa=0.08 low-speed turn 0.200 m 0.011 m
2.5 m/s, kappa=0.05, a=−1.5 0.235 m 0.042 m

Worst case 1.102 m → 0.065 m. The residual is the quintic polynomial approximating
a circular arc, which is inherent to the method.

Component consistency

quantity before after
sideslip from a 0.2 rad heading perturbation at 8 m/s vy = −1.589 m/s (11.5°) 0.000 m/s
same at 12 m/s vy = −2.384 m/s 0.000 m/s
ay − vx*yaw_rate at yaw_rate 0.48 rad/s, dvx = ±1 ±0.480 m/s² 0.000 m/s²
state vx vs target initial speed, scale = 0.8 / 1.2 ∓1.60 m/s ∓0.02 m/s

End-to-end

Batch of 64 with real random sampling (53 above the 2 m/s gate), smoothing on:

  • no NaN / Inf in ego_current_state, ego_agent_future, ego_agent_past
  • ego pose still exactly at the origin with heading 0 after centric_transform
  • vy max 0.5324 m/s — exactly the ±0.5 explicit vy perturbation range, i.e. the
    heading-induced component is gone
  • |ay − vx*yaw_rate| max 0.1032 m/s² — exactly the ±0.1 explicit ay noise range

Tests

tests/test_data_augmentation.py: 32 failed / 26 passed before and after — no
new failures. Those 32 are pre-existing on tier4-main: the tests
call StatePerturbation() with no arguments while the constructor requires five.

Behaviour changes to be aware of

  • Training targets change, so a checkpoint trained before this cannot be compared
    to one trained after without retraining.
  • The ego_past_noise_std semantics shift slightly. The scaled speed now shapes
    the first 2 s of the target, which is the point — the sample means "the vehicle
    really was travelling 10% faster", so its plan should start there too. The
    effective strength of that knob therefore changes.

Not in scope

Deliberately left alone, each worth a separate discussion:

  • The ±0.5 m/s vy perturbation range. It is a tuning decision, not a consistency
    bug, but Autoware reports twist.linear.y ~ 0 so it is unreachable in deployment
    (12.8° of sideslip at 2.2 m/s). Worth revisiting.
  • The |vx| >= 2.0 m/s gate that excludes the whole low-speed / standstill regime.
    Removing it needs a different formulation, not a different threshold: the method
    spreads the lateral offset over arc length, and a stopped vehicle has none.
  • ego_agent_past coordinate transform being commented out in centric_transform.
  • No mitigation for the history/recovery shortcut (copycat) that the upstream
    reference implementation addresses with M/N randomisation and a past-history bump.

Relation to the dev PR

The identical five commits are also proposed against dev; this branch is the same
set cherry-picked onto tier4-main (no conflicts, identical resulting diff).

Note the two branches already differ in this file for unrelated reasons, and this PR
touches neither:

  • the 4-column neighbors_future branch in centric_transform — present on
    tier4-main, absent on dev

🤖 Generated with Claude Code

rej55 and others added 6 commits July 31, 2026 18:02
… tangent

interpolation_future_trajectory() derived theta0 from the chord between the
perturbed position and the GT point 1 s ahead. That chord lags the true tangent
by roughly half the heading change over that second, and it ignores the heading
perturbation altogether, so the bridged target started off-tangent from the
state the model is conditioned on.

Use atan2(sin, cos) of the (perturbed) current state instead.

Measured on synthetic GT with ZERO perturbation, where the interpolation should
be the identity — max position error over the bridged 0-2 s window:

  8 m/s, kappa=0.03 turn   0.533 m -> 0.243 m
  8 m/s, kappa=0.06 turn   1.102 m -> 0.412 m
  3 m/s, kappa=0.08 turn   0.200 m -> 0.091 m

Straight-line cases are unaffected; the remaining error there comes from the
acceleration boundary conditions and is addressed separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…undary conditions

v0/a0/vT/aT were taken as vector magnitudes via torch.norm(). That was wrong in
two ways:

1. The sign was dropped, so a braking sample was handed a positive longitudinal
   acceleration and the bridged target overshot forward.
2. The lateral component was folded in. ay is the centripetal term v*omega, and
   the quintic already expresses that through the -v0*sin(theta0)*omega0 term,
   so the norm counted it twice. On a constant-speed curve the true tangential
   acceleration is exactly 0 while ||(ax, ay)|| = v^2*kappa — 3.84 m/s^2 at
   8 m/s on a 0.06 1/m curve.

ego_current_state is already in the body frame, so the tangential components are
a projection onto theta0. The terminal end gets the same treatment: project the
first/second finite differences onto the GT heading rather than taking their
magnitude.

Zero-perturbation regression (the interpolation must be the identity), max
position error over the bridged 0-2 s window:

  case                              before   after
  8 m/s straight a=-2 (decel)       0.553 m  0.031 m
  8 m/s kappa=0.03 turn             0.243 m  0.030 m
  8 m/s kappa=0.06 turn             0.412 m  0.065 m
  3 m/s kappa=0.08 turn             0.091 m  0.011 m
  2.5 m/s kappa=0.05 a=-1.5         0.232 m  0.042 m

(before = with the theta0 fix already applied; against the original code the
worst case was 1.102 m)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ay is the lateral (centripetal) acceleration and kinematically equals
vx * yaw_rate. augment() perturbs vx by up to +-1 m/s and leaves yaw_rate
untouched, but carried ay over unchanged, so the relation broke by
dvx * yaw_rate. At yaw_rate 0.48 rad/s that is +-0.48 m/s^2 — nearly five times
the +-0.1 m/s^2 the ay perturbation itself is allowed, i.e. the side effect of
the vx perturbation dominated the noise the augmentation meant to inject. The
resulting state described a vehicle whose curvature kappa = omega/v had changed
while its centripetal acceleration had not.

Rebuild ay from the perturbed speed and keep the sampled ay noise on top.
steering_angle is already recomputed from (yaw_rate, v_new) a few lines above,
so this brings ay in line with the treatment its siblings already get.

  v [m/s]  omega  dvx    required ay  ay before  ay after
  3.0      0.240  -1.0   0.480        0.720      0.480
  8.0      0.240  +1.0   2.160        1.920      2.160
  8.0      0.480  -1.0   3.360        3.840      3.360

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sideslip

The perturbed velocity and acceleration are body-frame quantities, but
centric_transform() rotates every vector by R(-delta_heading) into the new
perturbed-heading frame. Nothing compensated for that, so the velocity vector
kept pointing along the OLD heading while the body turned — a sideslip of
exactly delta_heading:

  v [m/s]  delta_heading  vy after transform  sideslip
  2.2      0.20 rad       -0.437 m/s          11.5 deg
  8.0      0.20 rad       -1.589 m/s          11.5 deg
  12.0     0.20 rad       -2.384 m/s          11.5 deg

A passenger car reaches a few degrees of sideslip at the limit, and Autoware
reports twist.linear.y ~ 0, so this state does not occur in deployment. The same
rotation also leaked longitudinal into lateral acceleration: ax = -2 m/s^2 at
delta_heading = 0.2 showed up as ay = +0.40 m/s^2.

Pre-rotate by R(+delta_heading) so the body-frame components survive the round
trip. vy is now 0.000 m/s for every speed and heading offset tested.

Note this makes the heading perturbation mean "the vehicle is yawed and moving
along its own heading", which is what a lateral tracking error looks like. The
+-0.5 m/s vy perturbation range is left as is — that is a tuning decision, not a
consistency bug, but it is worth revisiting since it is also unreachable in
deployment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed future

The ego_past_noise_std scaling was applied to inputs["ego_current_state"] after
interpolation_future_trajectory() had already consumed the unscaled state as its
start boundary condition. The state therefore reported one speed while the target
trajectory it is paired with started at another:

  scale  state vx   target initial speed   gap
  0.8    6.40 m/s   8.00 m/s               -1.60 m/s
  1.2    9.60 m/s   8.00 m/s               +1.60 m/s

That is up to +-20% of the speed, injected on half of the samples above the 2 m/s
gate, and it decouples the reported speed from the speed the plan starts with.

Move the scaling ahead of the interpolation and apply it to aug_ego_current_state
so the state that gets written back is the same one the target was built from. The
residual gap is now +-0.02 to 0.04 m/s, which is just the finite-difference
discretisation of the first trajectory step.

Note the intended semantics change slightly: the scaled speed now shapes the first
2 s of the target, which is the point — the sample means "the vehicle really was
travelling 10% faster", so its plan should start there too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Fumiya Watanabe <fumiya.watanabe.44@gmail.com>
@rej55 rej55 changed the title Fix/quintic kinematic consistency main fix(data_augmentation): quintic kinematic consistency Jul 31, 2026
scale_1d = scale.squeeze(-1) # (B_aug, 1)
inputs["ego_current_state"][aug_flag, 4:6] *= scale_1d # vx, vy
inputs["ego_current_state"][aug_flag, 6:8] *= scale_1d # ax, ay
aug_ego_current_state[aug_flag, 4:6] *= scale_1d # vx, vy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rej55
I noticed that there are several problem.

  1. in order for directly scaling XY to match the velocity difference, there are several assumption:
    • Ego past's t=0 is already in (0, 0) -- this is correct in default PR
    • This is a bug that will also affect tau-augmentation from my PR
  2. but self.augment(inputs) also modify the velocity with a diff, should we handle that, or just keep that discontinuity?

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.

2 participants