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.

  • In function compute(info) variable lastDistance is asigned a value that is never used.
    var lastDistance = distanceKalmanFilter.getLastEstimate();

    Is this intended?

  • it's intended. And you missread it. it's not: "var lastDistance" but "lastDistance" aka self.lastDistance

  • it's intended. And you missread it. it's not: "var lastDistance" but "lastDistance" aka self.lastDistance

    I don't understand your comment.

    In the code from on page 3 of this thread 

    function compute(info) {
    	altitude = info.altitude;
    	// Active Timer Values
    	if (timerState == Activity.TIMER_STATE_ON) {
    	    elapsedDistance = (info.elapsedDistance != null) ? (info.elapsedDistance) : (0);
    		// Calculate smooth Gradient and VAM, applying Simple Kalman Filter
    		if (elapsedDistance != 0) {
        		var lastAltitude = altitudeKalmanFilter.getLastEstimate();
        		var lastDistance = distanceKalmanFilter.getLastEstimate();
        		var currentAltitude = altitudeKalmanFilter.updateEstimate(altitude);
    			var currentDistance = distanceKalmanFilter.updateEstimate(elapsedDistance - lastElapsedDistance);
    			grade = (currentAltitude - lastAltitude) / currentDistance * 100;
    			vam = ((currentAltitude - lastAltitude) * 3600).toNumber();
    		}
    		lastElapsedDistance = elapsedDistance;
    	}
    }
     

    var lastDistance = distanceKalmanFilter.getLastEstimate();

    "lastDistance" is initialized, but not used after that. If there is no side effect in the method call "distanceKalmanFilter.getLastEstimate()" then it's unnecessary.

  • it's used in line #11

  • In line #11
    var currentDistance = distanceKalmanFilter.updateEstimate(elapsedDistance - lastElapsedDistance);
    the variables elapsedDistance and lastElapsedDistance are used but not lastDistance.

  • Hi there, I've tried to implement the Kalman filter mentioned in this topic and my calculated value seems to be very close to the one displayed in the native Garmin datafield. However, I'm curious whether somebody tried to make further smoothing of the calculated value. Let's say, if during the last 7 seconds the field displayed values [4, 4, 5, 4, 4, 5, 4] on a steady climb, it's obvious that the real value should be 4%. So has anybody tried to apply a moving average or any other low-pass filter after the Kalman filter to make the grade more steady? I understand that the final value will be more delayed in that case so curious to hear whether this makes some sense in real applications. Thx

  • You may want to display a single decimal point (eg: 4.6%). That'll give you a little more insight to how the data is changing under the covers. At least as you work on setting the filter values. There is always a tradeoff between response time and noise... the raw data you work with itself from the barometer seems to be chunky - coming through less often as we'd like. So the barometer data may get "stuck" at a particular value then jump. So what I've looked at doing it limiting the change rate so if the raw data changes by an amount that is obviously too much, I apply that change over several iterations to smooth that out to be more reasonable. I also have logic to ignore a single reading that appears to be erroneous. But I also prefer my grade calc to be more responsive.. it bugs me when it is still telling me I'm on a 12% climb after I've flattened out on the summit for a few seconds. Garmin really needs to just expose native grade to CIQ. Such a stupid decision to hide that key metric from us.

  • Thanks Dave, I agree that it's a strange decision from Garmin to not expose grade in the API, so implementing this field often literally takes longer than implementing the rest part of the Datafield app.

    I'll try to add one decimal point to investigate whether it displays [4, 4, 5, 4, 4, 5, 4] just because the grade is around 4.4% or because there is some significant measurement error that the Kalman filter is unable to smooth.

    Btw, how do you determine that the current measurement deviates too much - do you have some absolute limit in meters or relative limit in percent for altitude deviation?

  • I just calculate the max possible altitude change based on my distance covered between readings assuming a max grade of 25%. If the barometer reading is WAY off I ignore it. If the reading suggests say a 40% transient grade I limit the change in altitude to what would be 25% and then the next reading it likely back on track.

  • So do I correctly understand you that if the raw pre-filtered grade changes with the following values [2, 2, 3, 8, 8, 9, 10] then you are trying to smooth the transition between 3% and 8% by applying altitude from the first 8% occurrence a bit later? And if the raw pre-filtered grade changes with the following values [2, 2, 3, 27, 27, 30, 33] then you are throwing away everything that is more than 25% and waiting for the first <25% value to operate its altitude?