Skip to content

Position estimator corrections improvement and estimated velocity filtering#11387

Open
breadoven wants to merge 3 commits intoiNavFlight:maintenance-9.xfrom
breadoven:abo_pos_est_corr_improvement
Open

Position estimator corrections improvement and estimated velocity filtering#11387
breadoven wants to merge 3 commits intoiNavFlight:maintenance-9.xfrom
breadoven:abo_pos_est_corr_improvement

Conversation

@breadoven
Copy link
Collaborator

PR splits out handling of XY and Z position estimator corrections so the corrections are updated separately when new XY or Z sensor data is available.

Estimated EPH and EPV degradation logic also changed so EPH/EPV are only degraded if no sensor updates occur for >200ms rather than always degrading them between normal sensor update intervals.

Filtering has also been applied to the velocity estimates to remove some of the noise that seems to affect velocity in particular.

HITL tested and also on a quad without issues.

@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Position estimator corrections separation and velocity filtering

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Split XY and Z position estimator corrections into separate flags
  - Allows independent updates when XY or Z sensor data arrives
• Improved EPH/EPV degradation logic with 200ms timeout threshold
  - Only degrades when no sensor updates occur for >200ms
• Applied PT1 filtering to velocity estimates
  - Reduces noise in XY and Z velocity publications
• Optimized time macro for frequency-to-seconds conversion
Diagram
flowchart LR
  A["Sensor Updates<br/>XY & Z"] --> B["Split Correction<br/>Flags"]
  B --> C["Independent<br/>XY Corrections"]
  B --> D["Independent<br/>Z Corrections"]
  C --> E["Apply Corrections<br/>Per Axis"]
  D --> E
  E --> F["PT1 Filter<br/>Velocity"]
  F --> G["Publish Filtered<br/>Velocity"]
  H["200ms Timeout<br/>Logic"] --> I["Degrade EPH/EPV<br/>Only on Timeout"]
  I --> J["Update Estimates"]
Loading

Grey Divider

File Changes

1. src/main/common/time.h ✨ Enhancement +1/-1

Optimize frequency-to-seconds time macro

• Simplified HZ2S macro from nested macro calls to direct calculation
• Changed from US2S(HZ2US(hz)) to (1.0f / (hz)) for better efficiency

src/main/common/time.h


2. src/main/navigation/navigation_pos_estimator.c ✨ Enhancement +57/-34

Separate XY/Z corrections and add velocity filtering

• Split applyCorrections flag into applyCorrectionsXY and applyCorrectionsZ
• Added separate timeout tracking for XY and Z sensor updates with 200ms threshold
• Modified EPH/EPV degradation to only occur when sensor timeout exceeded
• Refactored correction application loop to handle XY and Z axes independently
• Added PT1 filtering to velocity estimates before publishing
• Moved accelerometer bias correction inside per-axis loop

src/main/navigation/navigation_pos_estimator.c


3. src/main/navigation/navigation_pos_estimator_flow.c ✨ Enhancement +1/-1

Update flow estimator for split correction flags

• Updated optical flow correction to use new applyCorrectionsXY flag
• Changed from generic applyCorrections to axis-specific flag

src/main/navigation/navigation_pos_estimator_flow.c


View more (1)
4. src/main/navigation/navigation_pos_estimator_private.h ⚙️ Configuration changes +4/-1

Add velocity filter constant and split correction flags

• Added INAV_EST_VEL_F_CUT_HZ constant for velocity filter cutoff frequency
• Split applyCorrections boolean into applyCorrectionsXY and applyCorrectionsZ

src/main/navigation/navigation_pos_estimator_private.h


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 2, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Float US2MS() in timeouts 📘 Rule violation ✓ Correctness
Description
The new sensor-update timeout checks mix float millisecond math (US2MS) with integer timeMs_t,
which can lose precision and yield incorrect timeout behavior after longer uptimes. This violates
the requirement to keep consistent units/types and avoid precision-losing conversions in math-heavy
code.
Code

src/main/navigation/navigation_pos_estimator.c[R764-767]

+    const bool XYSensorUpdateTimeout = US2MS(currentTimeUs) - lastXYSensorUpdateMs > 200;
+    ctx.newEPH = posEstimator.est.eph + ((posEstimator.est.eph <= max_eph_epv && XYSensorUpdateTimeout) ? 100.0f * ctx.dt : 0.0f);
+    const bool ZSensorUpdateTimeout = US2MS(currentTimeUs) - lastZSensorUpdateMs > 200;
+    ctx.newEPV = posEstimator.est.epv + ((posEstimator.est.epv <= max_eph_epv && ZSensorUpdateTimeout) ? 100.0f * ctx.dt : 0.0f);
Evidence
timeMs_t is an integer type, but the new timeout logic computes US2MS(currentTimeUs) using a
float macro and subtracts an integer timestamp, introducing mixed-type/precision behavior in core
estimator timing logic (rule 2).

src/main/navigation/navigation_pos_estimator.c[764-767]
src/main/common/time.h[36-38]
src/main/common/time.h[55-56]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new XY/Z sensor timeout checks use `US2MS(currentTimeUs)` which returns a float, then subtract `timeMs_t` integer timestamps. This mixes float and integer time units and can lose precision or behave unexpectedly over long uptimes.

## Issue Context
`timeMs_t` is `uint32_t`, while `US2MS()` is a float conversion macro. Timeout checks should be done using consistent integer time units.

## Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[764-767]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. last*SensorUpdateMs uses 0 📘 Rule violation ⛯ Reliability
Description
The new lastXYSensorUpdateMs/lastZSensorUpdateMs timestamps are initialized to 0, which is a
plausible real timestamp and conflates “no data yet” with “updated at boot”. This violates the
requirement to use explicit no-data sentinels for uninitialized external/sensor-driven state.
Code

src/main/navigation/navigation_pos_estimator.c[R745-747]

    estimationContext_t ctx;
+    static timeMs_t lastXYSensorUpdateMs = 0;
+    static timeMs_t lastZSensorUpdateMs = 0;
Evidence
The checklist requires explicit no-data sentinels; the new code uses 0 for last-update timestamps
even though an explicit sentinel (TIMEMS_MAX) exists for timeMs_t, making initialization
ambiguous (rule 1).

src/main/navigation/navigation_pos_estimator.c[745-747]
src/main/common/time.h[37-38]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`lastXYSensorUpdateMs` and `lastZSensorUpdateMs` are initialized to `0`, which is a plausible timestamp and does not explicitly represent the “no data yet” state.

## Issue Context
The codebase defines `TIMEMS_MAX` for `timeMs_t`, which can be used as an explicit sentinel, or you can add a boolean initialization flag and set the timestamps on first use.

## Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[745-747]
- src/main/navigation/navigation_pos_estimator.c[763-767]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Velocity filter not reset 🐞 Bug ✓ Correctness
Description
The new estimated-velocity PT1 filters are static-zero initialized and never reset to the current
velocity, so on first valid publish (and after eph/epv validity toggles) the published/controller
velocity transiently converges from 0 (or stale state) toward the true value. This creates a
predictable short-term bias/lag in the velocity used by navigation controllers.
Code

src/main/navigation/navigation_pos_estimator.c[R877-897]

        if (posEstimator.est.eph < positionEstimationConfig()->max_eph_epv) {
+            static pt1Filter_t estVelFilterState_X;
+            static pt1Filter_t estVelFilterState_Y;
+            float filteredVelX = pt1FilterApply4(&estVelFilterState_X, posEstimator.est.vel.x, INAV_EST_VEL_F_CUT_HZ, HZ2S(INAV_POSITION_PUBLISH_RATE_HZ));
+            float filteredVelY = pt1FilterApply4(&estVelFilterState_Y, posEstimator.est.vel.y, INAV_EST_VEL_F_CUT_HZ, HZ2S(INAV_POSITION_PUBLISH_RATE_HZ));
+
            // FIXME!!!!!
-            updateActualHorizontalPositionAndVelocity(true, true, posEstimator.est.pos.x, posEstimator.est.pos.y, posEstimator.est.vel.x, posEstimator.est.vel.y);
+            updateActualHorizontalPositionAndVelocity(true, true, posEstimator.est.pos.x, posEstimator.est.pos.y, filteredVelX, filteredVelY);
        }
        else {
            updateActualHorizontalPositionAndVelocity(false, false, posEstimator.est.pos.x, posEstimator.est.pos.y, 0, 0);
        }

        /* Publish altitude update and set altitude validity */
        if (posEstimator.est.epv < positionEstimationConfig()->max_eph_epv) {
+            static pt1Filter_t estVelFilterState_Z;
+            float filteredVelZ = pt1FilterApply4(&estVelFilterState_Z, posEstimator.est.vel.z, INAV_EST_VEL_F_CUT_HZ, HZ2S(INAV_POSITION_PUBLISH_RATE_HZ));
+
            const float gpsCfEstimatedAltitudeError = STATE(GPS_FIX) ? posEstimator.gps.pos.z - posEstimator.est.pos.z : 0;
            navigationEstimateStatus_e aglStatus = (posEstimator.est.aglQual == SURFACE_QUAL_LOW) ? EST_USABLE : EST_TRUSTED;
-            updateActualAltitudeAndClimbRate(true, posEstimator.est.pos.z, posEstimator.est.vel.z, posEstimator.est.aglAlt, posEstimator.est.aglVel, aglStatus, gpsCfEstimatedAltitudeError);
+            updateActualAltitudeAndClimbRate(true, posEstimator.est.pos.z, filteredVelZ, posEstimator.est.aglAlt, posEstimator.est.aglVel, aglStatus, gpsCfEstimatedAltitudeError);
Evidence
pt1FilterApply4 uses the existing filter state as prior output; for static filters that start at 0
and are never pt1FilterReset(), the first outputs are heavily biased toward 0 and converge
exponentially based on cutoff. Navigation code elsewhere demonstrates the expected pattern of
calling pt1FilterReset() on state transitions/resets.

src/main/navigation/navigation_pos_estimator.c[877-897]
src/main/common/filter.c[93-109]
src/main/navigation/navigation_multicopter.c[192-209]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `estVelFilterState_*` filters start with zero state and are never `pt1FilterReset()` to the current velocity. This creates a transient bias/lag in published/controller velocity on startup and after eph/epv validity toggles.

### Issue Context
`pt1FilterApply4()` uses previous `filter-&gt;state` as the prior output. Many other navigation filters explicitly call `pt1FilterReset()` on controller resets.

### Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[877-897]
- src/main/common/filter.c[93-109]

### Suggested change sketch
- Track a small init flag per axis (or reuse validity transitions):
 - When `posEstimator.est.eph` becomes valid and the XY filters haven’t been initialized, call:
   - `pt1FilterReset(&amp;estVelFilterState_X, posEstimator.est.vel.x);`
   - `pt1FilterReset(&amp;estVelFilterState_Y, posEstimator.est.vel.y);`
 - Same for Z when EPV becomes valid.
- Consider resetting when eph/epv becomes invalid to avoid reusing stale filter history after long invalid periods.
- Optionally move filter states to file-scope statics so they can be reset from `initializePositionEstimator()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

4. Debug velocity now inconsistent 🐞 Bug ✧ Quality
Description
After introducing filtered velocities for the published ‘actual’ state, DEBUG_POS_EST still logs raw
posEstimator.est.vel.* (unfiltered). This makes blackbox/debug traces inconsistent with what the
navigation controllers are consuming and complicates tuning/diagnosis.
Code

src/main/navigation/navigation_pos_estimator.c[R880-885]

+            float filteredVelX = pt1FilterApply4(&estVelFilterState_X, posEstimator.est.vel.x, INAV_EST_VEL_F_CUT_HZ, HZ2S(INAV_POSITION_PUBLISH_RATE_HZ));
+            float filteredVelY = pt1FilterApply4(&estVelFilterState_Y, posEstimator.est.vel.y, INAV_EST_VEL_F_CUT_HZ, HZ2S(INAV_POSITION_PUBLISH_RATE_HZ));
+
            // FIXME!!!!!
-            updateActualHorizontalPositionAndVelocity(true, true, posEstimator.est.pos.x, posEstimator.est.pos.y, posEstimator.est.vel.x, posEstimator.est.vel.y);
+            updateActualHorizontalPositionAndVelocity(true, true, posEstimator.est.pos.x, posEstimator.est.pos.y, filteredVelX, filteredVelY);
        }
Evidence
The PR changes updateActual* calls to use filtered velocities, but the DEBUG_SET fields remain based
on the raw estimator velocity values.

src/main/navigation/navigation_pos_estimator.c[877-918]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Velocity used by `updateActual*` is now filtered, but DEBUG_POS_EST continues to log raw `posEstimator.est.vel.*`, making logs inconsistent with controller inputs.

### Issue Context
This is a diagnosability/tuning concern introduced by the new filtering.

### Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[877-918]

### Suggested change sketch
- Either:
 - log filtered velocities instead of raw when publishing, or
 - add separate debug indices for filtered velocities while keeping raw ones.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +764 to +767
const bool XYSensorUpdateTimeout = US2MS(currentTimeUs) - lastXYSensorUpdateMs > 200;
ctx.newEPH = posEstimator.est.eph + ((posEstimator.est.eph <= max_eph_epv && XYSensorUpdateTimeout) ? 100.0f * ctx.dt : 0.0f);
const bool ZSensorUpdateTimeout = US2MS(currentTimeUs) - lastZSensorUpdateMs > 200;
ctx.newEPV = posEstimator.est.epv + ((posEstimator.est.epv <= max_eph_epv && ZSensorUpdateTimeout) ? 100.0f * ctx.dt : 0.0f);
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

1. Float us2ms() in timeouts 📘 Rule violation ✓ Correctness

The new sensor-update timeout checks mix float millisecond math (US2MS) with integer timeMs_t,
which can lose precision and yield incorrect timeout behavior after longer uptimes. This violates
the requirement to keep consistent units/types and avoid precision-losing conversions in math-heavy
code.
Agent Prompt
## Issue description
The new XY/Z sensor timeout checks use `US2MS(currentTimeUs)` which returns a float, then subtract `timeMs_t` integer timestamps. This mixes float and integer time units and can lose precision or behave unexpectedly over long uptimes.

## Issue Context
`timeMs_t` is `uint32_t`, while `US2MS()` is a float conversion macro. Timeout checks should be done using consistent integer time units.

## Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[764-767]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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