GRADE Calc - Filtered and Fit

I carefully watched the GARMIN Edge 1030 barometric sensor data on a ride yesterday.

Riding in Florida on an essentially FLAT road, the floating-point altitude metric bounces around +/- up to 2.5 feet each second.

The problem is that most even strong climbers, climb less than 1 foot per second. That is about a VAM of 1000 (1000 vertical meters per hour).

So the "noise" in the barometric sensor data can be twice as much as the typical climb rate. In other words, half the time, the "next" altitude reading suggests you are going down when you are on a steady steep climb.

I tried running a least squares line fit over 10 data points to get the GRADE "trend" that hopefully ignores the sensor noise. I didn't want to use more than 10 seconds or the response to changes in the road's grade would be too slow. But I still got a GRADE metric that bounced around too much.

So I just applied a "median value" filter, and that works great. I take the MEDIAN value of the last 4 plus the current altitude reading, as the altitude. Using the median does a good job of ignoring noise. And then I apply a least squares line fit to the last 10 filtered altitude values.

Anyway, if anyone is trying to create a useful GRADE metric in their CIQ apps, this seems to work well.

Since, for some reason, Garmin doesn't want to let us have access to the device's GRADE value.

  • Looking at your code, I have three quick questions.

    1. You use setInitialState() to set the filters to the current altitude and distance from activity info, at the start of the activity and after resuming (assuming after resumption that distance or altitude has changed)?

    2. do you ever use getLastEstimate()?

    3. In "updateEstimate" you set last_estimate = current_estiamte. Then you call updateProcessNoise(). So "a" will always be ZERO. And therefore "q" will always equal "q0". Is that intentional? If so, updateProcessNoise() isn't really needed?

  • 1. Yes. If you do not set the initial value, as filtered new value depends on previous values, any filter will need some time to stabilize around the actual value.

    2. Calling getLastEstimate() before calling updateEstimate(mea) recovers previous value necessary for grade calculation. So, it is not necessary to store this value out of the filter, but depending on your code, you can choose any of both options.

    3. You are right. It has no sense. I have made a mistake. updateProcessNoise() must be called before assigning new value to last_estimate so, updateEstimate is as follows (I have edited initial post with this class to correct this mistake):

    function updateEstimate(mea) {
    	kalman_gain = err_estimate / (err_estimate + err_measure);
    	current_estimate = last_estimate + kalman_gain * (mea - last_estimate);
    	err_estimate = (1.0 - kalman_gain) * err_estimate + abs(last_estimate - current_estimate) * q;
    	updateProcessNoise();
    	last_estimate = current_estimate;
    	return current_estimate;
    }

  • Thank you! I'll implement this soon. Hey - no biggie but you don't need to call ABS in updateProcessNoise (a*a is always positive). Cheers!

  • In this case ABS is not necessary, but depending on the model you use for q it could be. As I still have some doubts about final model, I keep it. But you can avoid it if you use an even power of a.

  • Hey .  Thanks For sharing.  I really like this approach.  Much simpler, uses less memory and also less processing than LS.  I've also moved it out of the class and into one function to further reduce memory footprint.  Running quite nicely on altitude and distance in the simulator against a previous workout (next will be testing in the wild).  Have you thought of using this approach to smooth out Power?  I've been playing around with it a bit tonight.  It's looking promising and again, uses less memory and processing than a moving average.  Attached is a comparison of raw power vs. kalman vs. moving average from a previous workout (err_est = 1.0, q = qo = 0.2, err_measure = 0.03)

  • Hi . Moving average (typically 3s) is used as standard for smoothing power values, so applying a different kind of filter could cause some incoherence. However, as I am not using any power meter now (hope to get one in the next months Relaxed), I have not been able to evaluate filtering on actual power data.

    In any case, as you mention, regardless the value you want to filter, computational and memory cost of moving average is increased when you consider more values. This implementation of the Kalman filter has always same cost and you can vary smoothing of the signal by only tuning the filter parameters.

    So, applying Kalman filter to power could be an interesting option if power readings are quite noisy and you need to significantly smooth it out 

  • Yeah, I implemented a 3-30 sec MA (user selectable - some people apparently prefer a long avg) for power in my DF's.  In the graphic I attached, the green line is a 3s MA.  You can see that the Kalman filter is pretty close to it, once it gets settled.  Power meters are interesting, they're extremely responsive to the slightest change.  If you sneeze it causes a spike.  If you shift in your seat it causes a spike, and so on, so many variables that affect a steady power reading, so anything that smooths some of those spikes (noise?) out is good AND preferably with little processing (and battery) cost.  Thanks very much for introducing me to the Kalman filter (thanks also to Dave for getting the conversation started).  It's really interesting.

  • Power data isn’t noisy. Noise implies variance around actual. Power data is accurate in general - just naturally varies as you adjust the power you are pushing into the strain gauges. So an N-sec average is the right approach I think, not a filter.

  • Thanks a lot for posting the code  ! Saved me a lot of time. Did you fine-tune the filter variables for your Garmin? Just wondering and maybe hoping to avoid too much experimenting.