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.

  • Thanks for this info. I have already tried to calculate grade based on altitude, ambient pressure and raw ambient pressure. All of them are noisy and I have only obtained good results applying least square line fitting for at least last 30s. Of course, it provides poor responsiveness when grade is not constant.

    Happy to know that 10s works fine. I will do some trials. BTW, what do you mean by "I take the MEDIAN value of the last 4 plus the current altitude reading"? Is it in fact the median of last 5 readings or the average of median for previous 4 readings and current one? 

  • The MEDIAN filtering was the key. 30 seconds, as you know, is just too long and lacks the responsiveness we desire when riding in the mountains and rollers. Yes, the last 5.... Or you can think of it as the prior 4 plus the one you just grabbed from Activity.info(). What I wanted to do is describe this as different from classic 3-median filtering in which every point is taken to be the median of the two points on either side. In our case, of course, we don't know the NEXT altitude value since we are streaming it real-time.

    Good luck and let me know how it works for you. In my current TEST DF, I actually have two User Settings to I can vary the size of the MEDIAN queue (default is 5) and the size of the Least Squares queue (default is 10). I may find I like different values for these 2 settings.

  • Hey ,

    I tested my GRADE algorithm and it is AMAZING. Granted I ride on mostly FLAT roads, but that was actually more problematic as the baro sensor was particularly noisy. However, the roads do undulate between +/-2% and I do have some "dome" like overpasses.

    What I set my queues to were 7 for the MEDIAN filtering (myELEV = median of the last 7 altitude readings). And then I take the Least Squares of the last 16 myELEV values for the GRADE.

    I was displaying my own GRADE right next to the Garmin Edge 1030's GRADE. And it matched essentially exactly. Except I found mine was more stable. For example, after riding up an overpass at 3.5% to 4%, then back down the other side, my GRADE would quickly resolve to around -3.5% to -4%. The Garmin's GRADE would sometimes bounce around between -2 and -7% grade. Which was not correct.

    So, anyway, I'm really happy with this and it seems as responsive as the internal GRADE, and actually more stable.

  • Thanks Dave, unfortunatelly I am off due to some issues on my edge and myself Disappointed and cannot test any algorith for grade in real world during next weeks.

    In any case, I have implemented two approaches for it:

    1) your proposal based on median that I will tune according to 7/16 values used by you

    2) applying a single order LP filter to grade that I calculate directly from altitude and elapsed distance readings. Numerical simulation works well even when I simulate quite noisy altitude values. Applying the filter to grade, and not altitude, tries to avoid noisy distance readings that I think could increase estimation error when riding at low speed or when distance is obtained from GPS. I am using a further linear regression on filtered grade estimations in order to avoid time delay introduced by the filter.

    As soon as I can test it, I will come back to show any results

     

  • GREAT ideas.... I didn't think of the GPS distance error... I assumed (wrongly) that distance was trustworthy. But now that you mention that... I did notice betore I implemented the median filter, than when testing in the car... when I slowed down toward an intersection, the grade would go wacky on me. I bet that was distance measurement issues. I might also apply a filter to smooth that out. But, it seems to be behaving really well now, so maybe not needed at cycling speeds. However, on my MTB on hard trails, my speed is slow enough I might find it becomes an issue. Keep in touch.

  • Hi Dave,

    I too am interested in your approach as I calculate Grade in my DF’s and it is ridiculously noisy.  Again, if only Garmin provided this in the API (sigh….).
    Anyway, maths is not my strong point, so I just want to make sure I understand what you’ve presented here.
    altitude and distance are calculated as the difference between the previous reading and the current reading.
    You say you take the Median of the last 7 altitude readings (last 6 plus current one).  Do you mean average of last 7 (which includes current altitude)?  I presume you store these in an array and pop the oldest one off the stack.
    At the same time you’re also storing X (as distance), Y (as altitude - from average of 7), X^2 and XY in a 16 position array (and popping the oldest values off the stack).  You then calculate slope “m” and Intercept “b”, using the summed X, Y, X^2 and XY values, to come up with a new “y” (altitude) coord from formula “y=mX + b” and then use this new ‘y’ value in the gradient calc: rise/run:  y / X?
    Is this your approach?
    Thanks,
    Rob.
  • I have done some trials to obtain accurate grade estimation. These are my conclusions:

    Altitude value obtained directly from info.altitude presents a significant noise. For devices with barometric sensor, ambientPressure and rawAmbientPressure are exposed, so altitude can be calculated from them.

    However, in case of using ambientPressure (low pass filtered pressure values) high delay and smoothing is introduced in altitude. As filter characteristics cannot be modified, ambientPressure is not valid for grade calculation.

    Obtaining altitude from rawAmbientPressure leads to same values as info.altitude, so it seems that Garmin calculates altitude using same raw data.

    Therefore, I see no reason to base calculation on other data than info.altitude

    Previously referred noise makes necessary to filter altitude. It can be done based on many approaches (i.e. the one used by Dave  using median filter plus linear regression)

    In my case, I have tried several IIR low pass filters (Butterworth, Chebysev) with different cutoff frequencies and orders. It has been difficult to reach a good compromise to have good responsiveness and not too much delay and smooting.

    So, finally I have tried Kalman filters and in this case, results are quite good. The algorithm only needs to track current and previous values for altitude and elapsed distance. I have not applied further regression.

    I am not sure about how Garmin calculates elapsedDistance (filtered or not), but as it is also susceptible of noise (especially if it is based on GPS data and not sensor) I decided to also filter distance.

    Following pseudocode shows how to obtain grade in this way:

    get new_altitude
    get new_elapsedDistande
    
    currentAltitude = altitudeFilter(new_altitude)
    currentDistance = distanceFilter(new_elapsedDistande)
    
    dA = currentAltitude - lastAltitude
    dX = currentDistance - lastDistance
    
    grade = dA / dX * 100
    
    lastAltitude = currentAltitude
    lastDistance = currentDistance
    
    

    If someone wants to use this kalman filter I provide the class I create for this purpose (it is based on work published by Denis Sene in following link)

    class SimpleKalmanFilter {
    	hidden var err_measure = 0.0;	//error of the measure
    	hidden var err_estimate = 0.0;	//error of the estimation. Updated in real time
    	hidden var qo = 0.0;			//Maximum Process noise
    	hidden var q = 0.0;				//Process noise
    	hidden var current_estimate = 0.0;
    	hidden var last_estimate = 0.0;
    	hidden var kalman_gain = 0.0;
    
    	function initialize(mea_e, est_e, _qo) {
    		err_measure = mea_e;
    		err_estimate = est_e;
    		qo = _qo;
    		q = _qo;
    	}
    
    	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;
    	}
    
    	function setInitialState(initial) {last_estimate = initial;}
    
    	function getLastEstimate() {return last_estimate;}
    
    	function updateProcessNoise() {
    		//Modify q according to process variation
    		//Make q=qo for a constant process noise
    		
    		var a = abs(last_estimate - current_estimate);
    		q = qo / (1 + a * a);
    	}
    
    	function abs(value) {
    		if(value < 0) {value = -value;}
    		return (value);
    	}
    
    }

    I have used this approach to calculate grade as part of one of my data fields for edge devices

    eGEAR+

  • AWESOME!! Thank you! I'm going to create a DF with your approach and my approach - showing the two grade values.. And create a screen on my Edge with yours, mine, and the internal system view of GRADE. It'll be a great test to see how they all track. I'm sure I'll end up using your approach - I really appreciate your work on this and publishing your code. It is too bad Garmin doesn't simply expose GRADE as an activity info metric.

  • In order to reduce battery drain and compute demand, I only run compute() every few seconds for many of my data fields. Is your Kalman Filter approach to GRADE calc sensitive to the frequency of running compute()? In other words, if compute were run, say, every 3 seconds, would you modify the algorithm to account for that?

  • No, the filter code does not depend on sampling frequency.

    However, notice that if you run compute at lower frequency than 1 Hz you will also refresh altitude and distance values at this reduced frequency. As consequence grade will be in some way averaged for the period between two runs. It is not caused by the filter but by sampling frequency.

    In any case, if you try this filter you could used following parameters to create the filters:

    hidden var altitudeKalmanFilter = new SimpleKalmanFilter(2.5, 0.50, 0.10);
    hidden var distanceKalmanFilter = new SimpleKalmanFilter(1.0, 0.10, 0.05);
    

    Checking the filter class you will see that I have modeled process noise as:

    function updateProcessNoise() {
    	//Modify q according to process variation
    	//Make q=qo for a constant process noise
    	var a = abs(last_estimate - current_estimate);
    	q = qo / (1 + a * a);
    }

    Both could be optimize, but I think it is a good starting point.

    It is possible that for lower sampling frequency, optimal values for qo and err_measure should be different than the ones I propose.

    Take into account that in order to avoid abnormal results during initial seconds, you must set initial state of the filter passing the first reading of the magnitud to be filtered when you run the filter for first time or you resume it after pausing.