Skip to content

calibrationd: use first block to only seed warp - #39170

Merged
sshane merged 7 commits into
commaai:masterfrom
sshane:calibrationd-uncertain-blocks
Oct 29, 2026
Merged

sshane merged 7 commits into
commaai:masterfrom
sshane:calibrationd-uncertain-blocks

Conversation

@sshane

@sshane sshane commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes a false "Device Remount Detected" right after initial calibration completes.

The first block of a fresh calibration is measured through an uncalibrated warp, where the model is least certain and can be well off, so the spread check fires once calibration completes. Now that block only moves the warp and isn't counted.

Over 1,558 fresh calibrations in 90 days of fleet data (1,066 dongles), block 0 was off by more than 1° in 28, and block 1 was within 1° of the next blocks in 99.7% (max 1.27°). 7 of 4,074 fresh calibrations raised the alert right at completion, and without block 0, 6 of them stay within the spread limits.

Trade-off: every fresh calibration takes one more block (~5s of straight driving).

mtbf_fresh_calib_blocks

We should continue to not require full certainty while calibrating, as some devices would then take minutes to calibrate

mtbf_fresh_calib_certainty

While valid_blocks < INPUTS_NEEDED, handle_cam_odom accepts observations
without the certainty check. That exemption (commaai#1227) is needed: block 0 is
collected through the RPY_INIT warp, where the model is too uncertain to
pass the check, so without it a device mounted far from RPY_INIT may never
start calibrating. But those blocks can be badly off, and as soon as
valid_blocks reaches INPUTS_NEEDED the spread check compares them to the
converged blocks and raises recalibrating ("Device Remount Detected")
although nothing moved.

Fleet qlogs, both on 0.11.x after calibration was wiped:
- 6ce263bb23aa6544/00000000--cbff53a1d4 (FORD_MAVERICK_MK1): 0% of block 0
  observations pass the certainty check (median angle std 0.85 deg vs
  0.25 deg limit), block 0 pitch 4.85 deg, blocks 1-4 ~10 deg. Pitch spread
  5.6 deg, recalibrating fires the moment validBlocks hits 5, settles at
  10.2 deg (same as before the wipe, mount unmoved on video).
- 129e61c64ce468e7/000000be--192db28fdf (RIVIAN_R1, hand over the camera at
  start): 18% of block 0 observations certain, block 0 pitch 1.5 deg, blocks
  1-4 ~7.5 deg, spread 6.3 deg, recalibrating at validBlocks 5, settles 7.6.

Fix: when valid_blocks reaches INPUTS_NEEDED, replace the exempt blocks
with their mean. rpyCalib is unchanged (the mean of those blocks is the
same), so convergence is untouched, but the spread check now compares new
blocks to the bootstrap estimate instead of to its worst block. A real
remount after that is still caught (test_calibration_auto_reset), and a
grossly wrong bootstrap (mean off by more than the spread limits) still
triggers a recalibration. With the logged values the merged bootstrap is
9.1 deg vs ~10.2 (Ford) and 6.3 vs ~7.7 (Rivian), well under the 4 deg
pitch limit.

Excluding exempt blocks from the spread check entirely was the
alternative, but that would also stop a grossly wrong bootstrap from ever
being corrected by the spread check.

Process replay: rpyCalib and calStatus are identical on all test segments,
only rpyCalibSpread changes (it no longer includes the bootstrap spread).
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Process replay diff report

Replays driving segments through this PR and compares the behavior to master.
Please review any changes carefully to ensure they are expected.

⚠️ 2 changed, 64 passed, 0 errors

Show changes

HYUNDAI - regenAA0FC4ED71E|2025-04-08--22-57-50--0 [calibrationd]

  extrinsicsCalibration.calPerc (234 diffs)
    frame 3: 2 -> 1
    frame 4: 3 -> 2
    frame 5: 4 -> 3
    frame 6: 5 -> 4
    frame 7: 6 -> 5
    frame 8: 7 -> 6
    frame 9: 8 -> 6
    frame 10: 9 -> 7
    frame 11: 10 -> 8
    frame 12: 11 -> 9
    (... 224 more)

  extrinsicsCalibration.calStatus (63 diffs)
    frame 174: calibrated -> uncalibrated
    frame 175: calibrated -> uncalibrated
    frame 176: calibrated -> uncalibrated
    frame 177: calibrated -> uncalibrated
    frame 178: calibrated -> uncalibrated
    frame 179: calibrated -> uncalibrated
    frame 180: calibrated -> uncalibrated
    frame 181: calibrated -> uncalibrated
    frame 182: calibrated -> uncalibrated
    frame 183: calibrated -> uncalibrated
    (... 53 more)

  extrinsicsCalibration.height.0 (220 diffs)
    frame 21: 1.2167283296585083 -> 1.2200000286102295
    frame 22: 1.2167283296585083 -> 1.2200000286102295
    frame 23: 1.2167283296585083 -> 1.2200000286102295
    frame 24: 1.2167283296585083 -> 1.2200000286102295
    frame 25: 1.2167283296585083 -> 1.2200000286102295
    frame 26: 1.2167283296585083 -> 1.2200000286102295
    frame 27: 1.2167283296585083 -> 1.2200000286102295
    frame 28: 1.2167283296585083 -> 1.2200000286102295
    frame 29: 1.2167283296585083 -> 1.2200000286102295
    frame 30: 1.2167283296585083 -> 1.2200000286102295
    (... 210 more)

  extrinsicsCalibration.rpyCalib.0 (200 diffs)
    frame 41: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 42: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 43: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 44: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 45: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 46: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 47: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 48: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 49: -4.796873781742761e-06 -> -9.593747563485522e-06
    frame 50: -4.796873781742761e-06 -> -9.593747563485522e-06
    (... 190 more)

  extrinsicsCalibration.rpyCalib.1 (200 diffs)
    frame 41: 0.008039150387048721 -> 0.008451230823993683
    frame 42: 0.008039150387048721 -> 0.008451230823993683
    frame 43: 0.008039150387048721 -> 0.008451230823993683
    frame 44: 0.008039150387048721 -> 0.008451230823993683
    frame 45: 0.008039150387048721 -> 0.008451230823993683
    frame 46: 0.008039150387048721 -> 0.008451230823993683
    frame 47: 0.008039150387048721 -> 0.008451230823993683
    frame 48: 0.008039150387048721 -> 0.008451230823993683
    frame 49: 0.008039150387048721 -> 0.008451230823993683
    frame 50: 0.008039150387048721 -> 0.008451230823993683
    (... 190 more)

  extrinsicsCalibration.rpyCalib.2 (200 diffs)
    frame 41: -0.0030142737086862326 -> -0.0036432072520256042
    frame 42: -0.0030142737086862326 -> -0.0036432072520256042
    frame 43: -0.0030142737086862326 -> -0.0036432072520256042
    frame 44: -0.0030142737086862326 -> -0.0036432072520256042
    frame 45: -0.0030142737086862326 -> -0.0036432072520256042
    frame 46: -0.0030142737086862326 -> -0.0036432072520256042
    frame 47: -0.0030142737086862326 -> -0.0036432072520256042
    frame 48: -0.0030142737086862326 -> -0.0036432072520256042
    frame 49: -0.0030142737086862326 -> -0.0036432072520256042
    frame 50: -0.0030142737086862326 -> -0.0036432072520256042
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.0 (200 diffs)
    frame 41: 9.593747563485522e-06 -> 0.0
    frame 42: 9.593747563485522e-06 -> 0.0
    frame 43: 9.593747563485522e-06 -> 0.0
    frame 44: 9.593747563485522e-06 -> 0.0
    frame 45: 9.593747563485522e-06 -> 0.0
    frame 46: 9.593747563485522e-06 -> 0.0
    frame 47: 9.593747563485522e-06 -> 0.0
    frame 48: 9.593747563485522e-06 -> 0.0
    frame 49: 9.593747563485522e-06 -> 0.0
    frame 50: 9.593747563485522e-06 -> 0.0
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.1 (200 diffs)
    frame 41: 0.0008241600007750094 -> 0.0
    frame 42: 0.0008241600007750094 -> 0.0
    frame 43: 0.0008241600007750094 -> 0.0
    frame 44: 0.0008241600007750094 -> 0.0
    frame 45: 0.0008241600007750094 -> 0.0
    frame 46: 0.0008241600007750094 -> 0.0
    frame 47: 0.0008241600007750094 -> 0.0
    frame 48: 0.0008241600007750094 -> 0.0
    frame 49: 0.0008241600007750094 -> 0.0
    frame 50: 0.0008241600007750094 -> 0.0
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.2 (200 diffs)
    frame 41: 0.0012578670866787434 -> 0.0
    frame 42: 0.0012578670866787434 -> 0.0
    frame 43: 0.0012578670866787434 -> 0.0
    frame 44: 0.0012578670866787434 -> 0.0
    frame 45: 0.0012578670866787434 -> 0.0
    frame 46: 0.0012578670866787434 -> 0.0
    frame 47: 0.0012578670866787434 -> 0.0
    frame 48: 0.0012578670866787434 -> 0.0
    frame 49: 0.0012578670866787434 -> 0.0
    frame 50: 0.0012578670866787434 -> 0.0
    (... 190 more)

  extrinsicsCalibration.validBlocks (216 diffs)
    frame 21: 1 -> 0
    frame 22: 1 -> 0
    frame 23: 1 -> 0
    frame 24: 1 -> 0
    frame 25: 1 -> 0
    frame 26: 1 -> 0
    frame 27: 1 -> 0
    frame 28: 1 -> 0
    frame 29: 1 -> 0
    frame 30: 1 -> 0
    (... 206 more)

  extrinsicsCalibration.wideFromDeviceEuler.0 (220 diffs)
    frame 21: 0.000636564043816179 -> 0.0
    frame 22: 0.000636564043816179 -> 0.0
    frame 23: 0.000636564043816179 -> 0.0
    frame 24: 0.000636564043816179 -> 0.0
    frame 25: 0.000636564043816179 -> 0.0
    frame 26: 0.000636564043816179 -> 0.0
    frame 27: 0.000636564043816179 -> 0.0
    frame 28: 0.000636564043816179 -> 0.0
    frame 29: 0.000636564043816179 -> 0.0
    frame 30: 0.000636564043816179 -> 0.0
    (... 210 more)

  extrinsicsCalibration.wideFromDeviceEuler.1 (220 diffs)
    frame 21: 0.016926465556025505 -> 0.0
    frame 22: 0.016926465556025505 -> 0.0
    frame 23: 0.016926465556025505 -> 0.0
    frame 24: 0.016926465556025505 -> 0.0
    frame 25: 0.016926465556025505 -> 0.0
    frame 26: 0.016926465556025505 -> 0.0
    frame 27: 0.016926465556025505 -> 0.0
    frame 28: 0.016926465556025505 -> 0.0
    frame 29: 0.016926465556025505 -> 0.0
    frame 30: 0.016926465556025505 -> 0.0
    (... 210 more)

  extrinsicsCalibration.wideFromDeviceEuler.2 (220 diffs)
    frame 21: -0.04259674996137619 -> 0.0
    frame 22: -0.04259674996137619 -> 0.0
    frame 23: -0.04259674996137619 -> 0.0
    frame 24: -0.04259674996137619 -> 0.0
    frame 25: -0.04259674996137619 -> 0.0
    frame 26: -0.04259674996137619 -> 0.0
    frame 27: -0.04259674996137619 -> 0.0
    frame 28: -0.04259674996137619 -> 0.0
    frame 29: -0.04259674996137619 -> 0.0
    frame 30: -0.04259674996137619 -> 0.0
    (... 210 more)

TOYOTA - regen218A4DCFAA1|2025-04-08--22-57-51--0 [calibrationd]

  extrinsicsCalibration.calPerc (140 diffs)
    frame 3: 2 -> 1
    frame 4: 3 -> 2
    frame 5: 4 -> 3
    frame 6: 5 -> 4
    frame 7: 6 -> 5
    frame 8: 7 -> 6
    frame 9: 8 -> 6
    frame 10: 9 -> 7
    frame 11: 10 -> 8
    frame 12: 11 -> 9
    (... 130 more)

  extrinsicsCalibration.calStatus (20 diffs)
    frame 123: calibrated -> uncalibrated
    frame 124: calibrated -> uncalibrated
    frame 125: calibrated -> uncalibrated
    frame 126: calibrated -> uncalibrated
    frame 127: calibrated -> uncalibrated
    frame 128: calibrated -> uncalibrated
    frame 129: calibrated -> uncalibrated
    frame 130: calibrated -> uncalibrated
    frame 131: calibrated -> uncalibrated
    frame 132: calibrated -> uncalibrated
    (... 10 more)

  extrinsicsCalibration.height.0 (220 diffs)
    frame 21: 1.2081996202468872 -> 1.2200000286102295
    frame 22: 1.2081996202468872 -> 1.2200000286102295
    frame 23: 1.2081996202468872 -> 1.2200000286102295
    frame 24: 1.2081996202468872 -> 1.2200000286102295
    frame 25: 1.2081996202468872 -> 1.2200000286102295
    frame 26: 1.2081996202468872 -> 1.2200000286102295
    frame 27: 1.2081996202468872 -> 1.2200000286102295
    frame 28: 1.2081996202468872 -> 1.2200000286102295
    frame 29: 1.2081996202468872 -> 1.2200000286102295
    frame 30: 1.2081996202468872 -> 1.2200000286102295
    (... 210 more)

  extrinsicsCalibration.rpyCalib.0 (200 diffs)
    frame 41: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 42: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 43: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 44: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 45: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 46: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 47: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 48: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 49: -3.475127741126016e-08 -> -6.950255482252032e-08
    frame 50: -3.475127741126016e-08 -> -6.950255482252032e-08
    (... 190 more)

  extrinsicsCalibration.rpyCalib.1 (200 diffs)
    frame 41: 0.0005723069189116359 -> 0.000818384054582566
    frame 42: 0.0005723069189116359 -> 0.000818384054582566
    frame 43: 0.0005723069189116359 -> 0.000818384054582566
    frame 44: 0.0005723069189116359 -> 0.000818384054582566
    frame 45: 0.0005723069189116359 -> 0.000818384054582566
    frame 46: 0.0005723069189116359 -> 0.000818384054582566
    frame 47: 0.0005723069189116359 -> 0.000818384054582566
    frame 48: 0.0005723069189116359 -> 0.000818384054582566
    frame 49: 0.0005723069189116359 -> 0.000818384054582566
    frame 50: 0.0005723069189116359 -> 0.000818384054582566
    (... 190 more)

  extrinsicsCalibration.rpyCalib.2 (200 diffs)
    frame 41: -0.0005243304185569286 -> -0.0006308543379418552
    frame 42: -0.0005243304185569286 -> -0.0006308543379418552
    frame 43: -0.0005243304185569286 -> -0.0006308543379418552
    frame 44: -0.0005243304185569286 -> -0.0006308543379418552
    frame 45: -0.0005243304185569286 -> -0.0006308543379418552
    frame 46: -0.0005243304185569286 -> -0.0006308543379418552
    frame 47: -0.0005243304185569286 -> -0.0006308543379418552
    frame 48: -0.0005243304185569286 -> -0.0006308543379418552
    frame 49: -0.0005243304185569286 -> -0.0006308543379418552
    frame 50: -0.0005243304185569286 -> -0.0006308543379418552
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.0 (200 diffs)
    frame 41: 6.950255482252032e-08 -> 0.0
    frame 42: 6.950255482252032e-08 -> 0.0
    frame 43: 6.950255482252032e-08 -> 0.0
    frame 44: 6.950255482252032e-08 -> 0.0
    frame 45: 6.950255482252032e-08 -> 0.0
    frame 46: 6.950255482252032e-08 -> 0.0
    frame 47: 6.950255482252032e-08 -> 0.0
    frame 48: 6.950255482252032e-08 -> 0.0
    frame 49: 6.950255482252032e-08 -> 0.0
    frame 50: 6.950255482252032e-08 -> 0.0
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.1 (200 diffs)
    frame 41: 0.0004921542131341994 -> 0.0
    frame 42: 0.0004921542131341994 -> 0.0
    frame 43: 0.0004921542131341994 -> 0.0
    frame 44: 0.0004921542131341994 -> 0.0
    frame 45: 0.0004921542131341994 -> 0.0
    frame 46: 0.0004921542131341994 -> 0.0
    frame 47: 0.0004921542131341994 -> 0.0
    frame 48: 0.0004921542131341994 -> 0.0
    frame 49: 0.0004921542131341994 -> 0.0
    frame 50: 0.0004921542131341994 -> 0.0
    (... 190 more)

  extrinsicsCalibration.rpyCalibSpread.2 (200 diffs)
    frame 41: 0.0002130478242179379 -> 0.0
    frame 42: 0.0002130478242179379 -> 0.0
    frame 43: 0.0002130478242179379 -> 0.0
    frame 44: 0.0002130478242179379 -> 0.0
    frame 45: 0.0002130478242179379 -> 0.0
    frame 46: 0.0002130478242179379 -> 0.0
    frame 47: 0.0002130478242179379 -> 0.0
    frame 48: 0.0002130478242179379 -> 0.0
    frame 49: 0.0002130478242179379 -> 0.0
    frame 50: 0.0002130478242179379 -> 0.0
    (... 190 more)

  extrinsicsCalibration.validBlocks (220 diffs)
    frame 21: 1 -> 0
    frame 22: 1 -> 0
    frame 23: 1 -> 0
    frame 24: 1 -> 0
    frame 25: 1 -> 0
    frame 26: 1 -> 0
    frame 27: 1 -> 0
    frame 28: 1 -> 0
    frame 29: 1 -> 0
    frame 30: 1 -> 0
    (... 210 more)

  extrinsicsCalibration.wideFromDeviceEuler.0 (220 diffs)
    frame 21: 0.0027249145787209272 -> 0.0
    frame 22: 0.0027249145787209272 -> 0.0
    frame 23: 0.0027249145787209272 -> 0.0
    frame 24: 0.0027249145787209272 -> 0.0
    frame 25: 0.0027249145787209272 -> 0.0
    frame 26: 0.0027249145787209272 -> 0.0
    frame 27: 0.0027249145787209272 -> 0.0
    frame 28: 0.0027249145787209272 -> 0.0
    frame 29: 0.0027249145787209272 -> 0.0
    frame 30: 0.0027249145787209272 -> 0.0
    (... 210 more)

  extrinsicsCalibration.wideFromDeviceEuler.1 (220 diffs)
    frame 21: 0.17452111840248108 -> 0.0
    frame 22: 0.17452111840248108 -> 0.0
    frame 23: 0.17452111840248108 -> 0.0
    frame 24: 0.17452111840248108 -> 0.0
    frame 25: 0.17452111840248108 -> 0.0
    frame 26: 0.17452111840248108 -> 0.0
    frame 27: 0.17452111840248108 -> 0.0
    frame 28: 0.17452111840248108 -> 0.0
    frame 29: 0.17452111840248108 -> 0.0
    frame 30: 0.17452111840248108 -> 0.0
    (... 210 more)

  extrinsicsCalibration.wideFromDeviceEuler.2 (220 diffs)
    frame 21: -0.0005038998788222671 -> 0.0
    frame 22: -0.0005038998788222671 -> 0.0
    frame 23: -0.0005038998788222671 -> 0.0
    frame 24: -0.0005038998788222671 -> 0.0
    frame 25: -0.0005038998788222671 -> 0.0
    frame 26: -0.0005038998788222671 -> 0.0
    frame 27: -0.0005038998788222671 -> 0.0
    frame 28: -0.0005038998788222671 -> 0.0
    frame 29: -0.0005038998788222671 -> 0.0
    frame 30: -0.0005038998788222671 -> 0.0
    (... 210 more)

Replaces averaging the bootstrap blocks. The first block of a fresh
calibration is measured through an uncalibrated warp, where the model is
least certain and can be well off. It now only moves the warp and isn't
counted, so it can't trip the spread check or bias the calibration.

Over 174 fresh calibrations in 5 days of fleet qlogs (137 dongles), block 0
was off by more than 1 deg in 3, and from block 1 on every block was within
1 deg of where calibration settled (max 0.77 deg). The two with a false
remount had block 0 off by 5.3 and 6.2 deg pitch.

Costs one extra block (~5s of straight driving) on every fresh calibration.
… master already does), fixes master percent dropping on recalib
@sshane sshane changed the title calibrationd: don't treat uncertain bootstrap blocks as a remount calibrationd: use first block to only seed warp Oct 29, 2026
@sshane
sshane merged commit 371e4ef into commaai:master Oct 29, 2026
12 of 13 checks passed
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