Skip to content

fix(ekf2): avoid failover to an instance with a failing test ratio [1.18] - #28533

Merged
dakejahl merged 1 commit into
PX4:release/1.18from
Saibernard:backport-1.18-ekf2-selector
Sep 4, 2026
Merged

fix(ekf2): avoid failover to an instance with a failing test ratio [1.18]#28533
dakejahl merged 1 commit into
PX4:release/1.18from
Saibernard:backport-1.18-ekf2-selector

Conversation

@Saibernard

Copy link
Copy Markdown
Contributor

Backport of #28418 to release/1.18, the same commit as on main.

The selector could fall back to an instance whose vertical state was already failing, which is what produced the altitude spikes in #27013.

…X4#28418)

* fix(ekf2): keep evaluating instance selection while the primary is unhealthy

The selection block only ran when UpdateErrorScores() reported a change:
a new instance appearing or a health transition. A primary that stops
publishing produces exactly one such transition, on the cycle its
timeout is detected. If no switch happens on that cycle, nothing sets
updated again: the stable alternatives do not count as primary updates
and the timed out instance is skipped thereafter, so the fallback logic
is never evaluated again even though the module keeps being scheduled.

Today that single evaluation always resolves the situation, because the
fallback switches unconditionally to the best healthy candidate on that
same cycle. But any selection policy that can decline to switch on the
transition cycle, for example one that waits out a transient fault,
needs the decision re-evaluated while the primary remains unhealthy.
Re-enter the selection block whenever the selected instance is
unhealthy. The decisions inside are unchanged and switching is
idempotent, so behaviour today is identical.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* fix(ekf2): do not fail over to an instance with a sustained test ratio failure

When the primary EKF goes unhealthy the selector falls back to the best
instance that is healthy, and healthy only requires zero filter fault
flags and a positive combined test ratio. An instance whose test ratio
has been failing for a long time therefore remains a first class switch
target even though its state can be far from the truth.

b7efd4f introduced this on purpose for the switch-away direction: a
test ratio at or above one became a warning rather than ill health, so a
transient ratio spike cannot hard fail an instance, and a warned primary
is left through the lower relative error path once the warning has been
sustained for one second. What that commit did not do is apply the same
reasoning to the switch-to direction. The candidate loop only filters on
healthy, so a brief hard fault on the primary, for example transient
accelerometer clipping, sends the selector straight to a diverged
instance.

That is the mechanism behind the repeated altitude jumps in issue 27013:
one instance had stopped fusing baro, its vertical state up to 155.6 m
from the other instance while its combined test ratio sat pegged at 2,
and each of the seven short clipping faults on the good instance bounced
the selector back to it (15 instance switches in total counting the
returns), the worst switch stepping the published altitude by 128.5 m
and provoking a hard TECS reaction.

Classify fallback candidates with the same sustained warning test the
switch-away trigger already uses. When the primary goes unhealthy, fail
over immediately to the best candidate without a sustained warning; the
different IMU preference is kept within each tier, and a candidate
without a sustained warning is preferred even over a warned candidate on
a different IMU, since a warned instance is the one known to be
diverging. A sustained warned candidate is accepted in two cases only:
the primary has timed out entirely, where frozen attitude and position
outputs are worse than any live alternative, or the primary has been
continuously unhealthy for kWarnedFallbackDelay (five seconds), so a
brief fault rides out on the current state while a persistently faulted
primary still gets the least bad alternative rather than none. The
re-evaluation of this decision while the primary stays unhealthy is
provided by the previous commit.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

* test(ekf2): add a functional test for the instance selector

The selector had no test at any level. This drives EKF2Selector through
published multi instance estimator_status messages on the real work
queue and observes estimator_selector_status, so the selection policy is
exercised without a simulator.

The scenarios encode the failure pattern from issue 27013 and the
no-whipsaw property discussed there: a clean fallback on a hard primary
fault stays immediate, the switch away from a degraded primary through
the sustained warning path still works, three separate brief hard faults
on the primary no longer bounce the selector to an instance whose test
ratio has been failing for seconds, a primary that stops publishing
falls back to the degraded instance without delay, and a primary that
stays hard faulted for longer than the ride-out window still falls back
rather than being kept forever.

The scenario setup helpers run until the selector reaches the intended
starting state rather than assuming fixed timings, since the health
hysteresis and warning windows run on wall clock time. The fault-clear
windows exceed the selector's one second healthy hysteresis so the
faults are genuinely separate and the unhealthy-since tracking restarts
between them. The harness waits until the work queue manager actually
serves queues before constructing the selector: a fixed delay races the
manager startup on a loaded runner.

to run: make tests TESTFILTER=EKF2Selector

Assisted-by: Claude:claude-fable-5
Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>

---------

Signed-off-by: Saibernard Yogendran <bernie97@seas.upenn.edu>
(cherry picked from commit 436cc71)
@github-actions github-actions Bot added kind:bug Something is broken or behaving incorrectly. kind:test Adds or improves tests. scope:estimation EKF, local position, attitude, wind, bias, or aiding logic. scope:testing Unit, integration, fuzzing, or test data. labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🔎 FLASH Analysis

px4_fmu-v5x [Total VM Diff: 344 byte (0.02 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +344  +0.0%    +344    .text
     +28%    +368   +28%    +368    EKF2Selector::Run()
    +3.6%      +8  +3.6%      +8    EKF2Selector::SelectInstance()
    +0.2%      +4  +0.2%      +4    EKF2Selector::EKF2Selector()
    +2.0%      +4  +2.0%      +4    EKF2Selector::PublishEstimatorSelectorStatus()
    +0.9%      +4  +0.9%      +4    EstimatorInterface::setAirspeedData()
    +0.8%      +4  +0.8%      +4    EstimatorInterface::setRangingBeaconData()
    +4.8%      +4  +4.8%      +4    FlightTask
     +44%      +4   +44%      +4    g_nullstring
    -0.0%      -4  -0.0%      -4    g_cromfs_image
    -1.4%      -8  -1.4%      -8    EKF2Selector::PublishVehicleGlobalPosition()
    -0.0%     -12  -0.0%     -12    [section .text]
    -4.8%     -32  -4.8%     -32    EKF2Selector::PublishVehicleAttitude()
  +0.0%     +92  [ = ]       0    .debug_abbrev
  +0.0%      +8  [ = ]       0    .debug_frame
  +0.0%    +348  [ = ]       0    .debug_info
  +0.0%    +640  [ = ]       0    .debug_line
     +33%      +1  [ = ]       0    [Unmapped]
    +0.0%    +639  [ = ]       0    [section .debug_line]
  +0.0%    +557  [ = ]       0    .debug_loclists
  +0.0%    +123  [ = ]       0    .debug_rnglists
    [NEW]      +3  [ = ]       0    [Unmapped]
    +0.0%    +120  [ = ]       0    [section .debug_rnglists]
  +0.0%    +232  [ = ]       0    .debug_str
  -4.0%    -344  [ = ]       0    [Unmapped]
  +0.0% +1.95Ki  +0.0%    +344    TOTAL

px4_fmu-v6x [Total VM Diff: 336 byte (0.02 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +336  +0.0%    +336    .text
     +28%    +368   +28%    +368    EKF2Selector::Run()
    +3.6%      +8  +3.6%      +8    EKF2Selector::SelectInstance()
    +0.2%      +4  +0.2%      +4    EKF2Selector::EKF2Selector()
    +2.0%      +4  +2.0%      +4    EKF2Selector::PublishEstimatorSelectorStatus()
    +0.9%      +4  +0.9%      +4    EstimatorInterface::setAirspeedData()
    +0.8%      +4  +0.8%      +4    EstimatorInterface::setRangingBeaconData()
    +4.8%      +4  +4.8%      +4    FlightTask
    -0.0%      -4  -0.0%      -4    g_cromfs_image
    -1.4%      -8  -1.4%      -8    EKF2Selector::PublishVehicleGlobalPosition()
    -0.0%     -16  -0.0%     -16    [section .text]
    -4.8%     -32  -4.8%     -32    EKF2Selector::PublishVehicleAttitude()
  +0.0%     +92  [ = ]       0    .debug_abbrev
  +0.0%      +8  [ = ]       0    .debug_frame
  +0.0%    +348  [ = ]       0    .debug_info
  +0.0%    +648  [ = ]       0    .debug_line
    [NEW]      +5  [ = ]       0    [Unmapped]
    +0.0%    +643  [ = ]       0    [section .debug_line]
  +0.0%    +476  [ = ]       0    .debug_loclists
  +0.0%    +120  [ = ]       0    .debug_rnglists
  +0.0%    +232  [ = ]       0    .debug_str
  -5.8%    -336  [ = ]       0    [Unmapped]
  +0.0% +1.88Ki  +0.0%    +336    TOTAL

Updated: 2026-09-04T01:35:45

@dakejahl
dakejahl merged commit e4bacc6 into PX4:release/1.18 Sep 4, 2026
73 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind:bug Something is broken or behaving incorrectly. kind:test Adds or improves tests. scope:estimation EKF, local position, attitude, wind, bias, or aiding logic. scope:testing Unit, integration, fuzzing, or test data.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants