RTKLIB solution accuracy

OK, so let’s say you’ve just gone out and collected some decent raw measurement data, everything looks like it is working properly and you get a nice-looking solution from RTKLIB, maybe something like this.


The next thing you might want to know is how accurate is that solution?  Fortunately, RTKLIB does include accuracy estimates in the solution file.  These are expressed as standard deviations, one for each axis.  Here’s an example for a solution calculated in ENU (east/north/up) coordinates.  The sde, sdn, and sdu columns are the standard deviation estimates for each solution point and are given in meters.  If the solution were calculated in XYZ coordinates, the columns would be labelled sdx, sdy, and sdz.


You can display these on the solution plot with RTKPLOT by setting “Error Bar/Circle” in the options menu to “Dots” or “Bar/Circle”.  You can see the gray lines in the plot below that represent the solution value plus and minus one standard deviation.


Visually, I prefer to see three standard deviations plotted, rather than just one.  Three standard deviations represents 99.7% of the distribution, so the correct value should nearly always appear between the two gray lines.  One standard deviation, on the other hand,  represents only 68% of the distribution which is hard for me to attach any physical meaning to.  This can be easily changed in RTKLPLOT by replacing SQRT(s[j]) with 3*SQRT(s[j]) all three places it appears in the plotdraw.cpp file and recompiling RTKPLOT which gives this:


Of course these are just estimates of the solution uncertainty made by the kalman filter in RTKLIB and we can not assume they are accurate without some analysis.  They are a function of the input parameters that set the input measurement uncertainties and process noises for the kalman filter.  These are all in the “stats” section of the input configuration file.  Even if these are all set to correctly match your actual configuration, there will be additional errors in these estimates caused by non-linearities in the system, non-gaussian distributions, and time-correlated measurements among other things.

So, let’s do a little analysis to get a feel for how useful these estimates are.  We’ll start with the above data set which includes eight hours of measurements from a stationary u-blox M8T receiver and a CORS reference station 8 km away.  I enabled fix-and-hold for ambiguity resolution with relatively low tracking gain (pos2-varholdamb=0.1).  I ran both static and kinematic solutions on the data, then turned off ambiguity resolution and re-ran the solutions to get examples of both fixed and float solutions for static and kinematic modes.

After running the solutions, I pulled the solution files into matlab for plotting and analysis.  Since the rover was stationary and I knew it’s precise location I was able to easily calculate the true errors.  I then plotted for each solution point, the absolute value of the errors and three times the standard deviation estimate for each point, similar to the above RTKPLOT plot.  The raw data is in E/N/U coordinates but I combined the East and North values into a single horizontal error and error estimate.  After finding that in some cases the error estimates were too low, I also plotted a line using double the error estimate, to see if that might be a usable value .  In the title of each plot I’ve included the percent of samples that were inside the error lines, the first number is for the undoubled estimate, the second is for the doubled estimate.  I also made one more modification to the error estimates by limiting them to a minimum value.   In the case of the static solutions, the RTKLIB error estimate will continue to decrease almost all the way to zero but this is not realistic since there are errors that are not visible to the solution.  I arbitrarily limited each horizontal axis standard deviation to no less than 5 mm, and the vertical axis to 1 cm.   I don’t have good justification for these particular numbers but do believe there needs to be some limits set.

For a perfect Gaussian distribution, plus or minus three standard deviations should include 99.7% of the data.  Given that these are not perfect distributions and will also contain outliers, I would consider the estimates useful if more than 95% of the data fell within them.

Here are the plots for the above data set run with static solutions, float on the left, and only the fixed values of the solution with ambiguity resolution enabled on the right.  The blue line is the actual error, the red line is the RTKLIB estimate, and the yellow line is double that.  The top plots are for the horizontal axes and the bottom plots for the vertical axis.  In the static cases, all the error lines dropped below the minimum estimate values I set above fairly quickly, so only the initial convergence of the float solution was really of any interest.   Still the error estimates in that region look somewhat reasonable, although the undoubled estimates are clearly optimistic. The undoubled estimates (red) are not visible on the plots where both estimates have dropped below the minimum and they get overwritten by the doubled estimate.


Next are the same plots for the kinematic solutions.  Just to confuse you, I have accidentally put the float solution on the right this time.  Again, the solution with ambiguity resolution enabled (on the left) only includes points that were fixed.  These are more interesting than the previous plots since they are not dominated by the arbitrary minimum error estimate limits I added.  Looking at the float solution first, the undoubled estimates again look too optimistic with only 92.2% and 75.2% of the data within that limit.  At 100% and 99.6% the doubled values (yellow lines) appear to be better estimates.  For the fixed values on the left however the undoubled estimate percentages and plot lines appear to be fairly reasonable.


In one last example, I used a different data set, this one is one of the sample data sets on my website which was taken with two M8T receivers, one on each end of a kayak out on the ocean.  In this case we know that the distance between the two receivers will remain constant so we can use that information to do a similar analysis as above.  Because the kayak is moving in all three axes with the waves we can not separate the horizontal and vertical components but can do an actual error to estimated error comparison for the three axes combined.

The float solution is back on the left again, just the fixed values from the ambiguity resolution enabled solution on the right.  Based on the calculated percentages and the shapes of the curves, it seems fairly reasonable to again choose the doubled estimates for the float solution and the undoubled estimates for the fixed values.  Even with the doubling, the percent of solution values within three standard deviations is still only 88.5% which is rather low but most of the outliers were in the initial convergence where we might expect more issues.


So, based on these few examples I do see correlation between the RTKLIB error estimates and the actual errors, not as much as I would have liked, but there is some.   Maybe these adjustments I made would hold up for other cases as well, but I would not have a lot of confidence without trying more examples first.

Clearly these error estimates can not be taken at face value.  Do they have any value?  It probably depends on what you are trying to do with them.  If you are just trying to get a rough idea of the accuracy of the measurements, especially if it is in a statistical sense, and you have done some previous analysis with similar data sets, then you can most likely derive some value from them.  If the absolute accuracy of a single measurement is important to you then you will probably need to find another solution.

What about trying to adjust the input configuration parameters (measurement and process noises) to make the accuracy estimates more accurate?  You may be able to make small improvements but I suspect they will not be significant enough to avoid post-solution adjustments.  You will also find that adjusting these parameters will affect the quality of the solution and it will be difficult to optimize for both.

I am interested, though, if anyone has any ideas for simple (or at least only moderately complicated) code changes that could be made to RTKLIB to improve these estimates or any other ideas on how to improve them.





New code, new gps data

I’ve just released the b28 version of the demo5 code.  It includes all of the updates in the b28 update of the official RTKLIB 2.4.3 code.  It also now supports both Tersus and Swift low cost dual frequency receivers.  The Tersus updates were part of the official 2.4.3 release although I did make a few changes to fully support the L2 measurements as well as some changes to the makefiles to get all apps to build.  The Swift receiver support is based on code I pulled from the Swift RTKLIB Github page.  I suspect the receiver specific RTKLIB code for both receivers could use some improvements in translating from the raw binary formats to the Rinex/internal observation formats but at least this is a starting point.  The executables are available from the download section at rtkexplorer.com.  The source code is available on my Github page.

I’ve also uploaded some data sets comparing the Swift Piksi Multi and the u-blox M8T as well as between the Tersus BX306 receiver and the u-blox M8T.  This data is also available from the download section at rtkexplorer.com.



RTKLIB on a drone with u-blox M8T receivers

Drones are a popular application for RTKLIB and quite a few readers have shared their drone-collected data sets with me, usually with questions on how they can get better solutions. In many cases, the quality of this data has been fairly poor and it has been difficult to get satisfactory results. I was curious to understand why this environment tends to be so challenging since in theory a drone should have more open skies than just about any other application.

To do an experiment, I bought an inexpensive consumer drone from Amazon. I chose the X8C from Syma since it is beginner model and a little larger than some options. I figured the larger size should make it better able to carry some extra weight.

After a few practice flights to get the hang of flying it, I used some duct tape and double-sided foam adhesive to attach a u-blox antenna and 90 mm diameter ground plane to the top of the drone and a u-blox M8T receiver with my custom CHIP data logger underneath where the camera usually goes. I used the landing gear as a spool to wind the unnecessary five meters of antenna cable which was the heaviest part of the whole setup. From a weight perspective, the Emlid Reach units would have been a better choice, but I wanted to collect data from the Galileo constellation of satellites as well as GPS and GLONASS so I used my CSG receiver with the newer 3.0 firmware. I used a second CSG receiver mounted on top of my car as the base station.  Here’s a stock photo of the drone on the left and after my modifications on the right.


Although the drone was able to lift the extra weight fairly easily, it seemed to affect the stability of the flight control system and after a few minutes the prop motors would start to fight each other. At that point the drone would start to descend even at full throttle and the drone would land hard enough to usually bounce on its side or back. Still I was able to make a number of short flights which were adequate for testing purposes.

Here’s the observation data for the first set of flights, base station on the left and drone on the right. Red ticks are cycle-slips and gray ticks are half-cycle ambiguities. Ideally, the drone data would look as clean as the base but as you can see it is significantly worse and it turned out to be unusable for any sort of reliable position solution.  The regions without cycle-slips in the drone observations are the times in between flights in which the drone is sitting on the ground.


Clearly, while the drone is flying, something is interfering with the GPS receiver or antenna, most likely either EMI or mechanical vibration. I could have used a fancy test stand and RF sniffer to try and locate the source of interference but since this blog is focused on low-cost solutions I just used some duct tape, a steel bar, and the RTKLIB code instead.

I used two types of duct tape, both the polyester/fabric type that everyone calls duct tape, and also the metal foil type that is actually used to repair or install ducts. I first used the non-metal duct tape to securely attach the landing gear to the heavy steel bar. The steel bar was convenient because it was easy to attach but anything heavy enough to prevent the drone lifting off under full throttle would work fine.

I then started an instance of RTKNAVI on my laptop and connected it to the receiver on the drone.  The goal was to simulate a complete drone flight while the drone was sitting on the ground and at the same time watch the RTKNAVI observations to detect any degradation of the measurements.  I used a wireless connection but a USB cable would have worked too.

Unfortunately RTKNAVI won’t plot the observation data real-time, but by selecting the tiny “RTK Monitor” box in the bottom left corner of the main RTKNAVI screen, then choosing “Obs Data” from the menu, I was able to get a continuously updating listing of the observations.  Cycle-slips show up as non-zero values in the first column with the I heading. I chose a location outdoors with open enough skies that any degradation in the observation data would be obvious.


I first observed the cycle-slip column with the drone powered down to verify I wasn’t getting any cycle-slips on all but the lowest elevation satellites. I then continued to observe the cycle-slip column while sequencing through the steps required to fly the drone. I first powered on the drone, then powered on the transmitter, then issued the calibration/connection sequence, then turned on the throttle to low. So far, so good, no sign of cycle-slips. I then started moving the joysticks to issue steering commands which caused the motors to change speeds. All of a sudden I started getting cycle-slips, the more aggressive the steering commands, the more cycle-slips I saw. Aggressive changes in throttle also caused cycle-slips but full throttle with no adjustments or steering commands was fine.

Next I moved just the antenna, then just the receiver away from the drone while issuing steering commands. Moving the antenna away had no effect but moving the receiver away eliminated the cycle-slips.

At this point my guess was that the interference was coming from the relatively high power switching in the motor control circuits and that the antenna ground plane was shielding the antenna from this interference but nothing was shielding the receiver. To test this theory, I attached a layer of the metal duct tape to the bottom of the drone to act as a shield between the drone controller board and the receiver.  I then re-attached the receiver to the bottom of the drone and re-ran the experiment. This time there were almost no cycle-slips even with the most aggressive steering.

I then removed the steel bar and ran a second set of short flights with the layer of metal tape still in place. I was a little concerned that the new shield would interfere with commands sent from the transmitter to the drone so I first tested everything while still on the ground and then kept the drone fairly close during the flight. Fortunately I didn’t see any sign of commands not getting through.

The drone data looked much cleaner in this flight!  Unfortunately, this time the base data was no good with many simultaneous cycle-slips throughout the observation data. At this point I realized that I had placed the base station receiver directly on the top of the car when collecting the data which was very hot at the time. Usually I keep the receiver in the car to avoid this and only place the antenna on the roof. I have seen this kind of severe temperature effects cause simultaneous cycle-slips before but never to this extent. Again the data was completely unusable.

So, back out there again for a third round of flights. This time, everything looked much better. I still saw a few cycle-slips, especially when first applying the throttle at take-off, so my shielding was not perfect but a dramatic improvement over the first flight. The plots below show the results. The top two plots are position solutions for the z-axis. The top plot is with continuous ambiguity resolution and the middle plot is with fix-and-hold enabled. The bottom plot is the drone observation data.


I made three adjustments to the input configuration file from what I would normally use for my car based measurements.  First of all, since the drones have very open skies, I adjusted the minimum elevation angles from 15 degrees to 10 degrees.   Then, after plotting and observing the accelerations from an initial solution, I increased the vertical acceleration dynamics estimate (stats-prnaccelv) from 0.25 to 1.0.  Finally, because I was seeing slightly higher position variances in the initial solution than I usually do, I adjusted the position variance AR threshold (pos2-arthres1) from 0.004 to 0.1  Both of these last two changes would make sense if the level of vibration were higher in the drone than I am used to seeing, which is quite likely.

Each time the drone landed/crashed due to the unstable flight control system it would bounce to the side or upside-down and that is what is causing the cycle-slips and switch from fix to float at the end of each flight. As you can see though in every case I quickly get another fix after I put the drone upright again. The fixes are solid enough to hold through the entire flight even in continuous mode for all but one of the flights. With fix-and-hold enabled all flights maintained 100% fix rate. The data is as good as or better than similar experiments where I have mounted the rover on top of a car.

This is not surprising since the skies are more open in this experiment. Having over twenty satellites available for ambiguity resolution also helped. I used all the satellites (GPS/GLONASS/Galileo/SBAS) for ambiguity resolution and took advantage of the new feature in the demo5 b26 code that cycles through all the satellites and will throw a single one out if it is preventing a fix. This will automatically occur anytime the number of satellites available for ambiguity resolution is greater than the config parameter “pos2-mindropsats” which defaults to twenty.

I have added the raw data and the configuration file to the  sample data set section at rtkexplorer.com

I imagine different drones will have different issues and not all will be as easy to fix as this one, but the method described here or something similar should be helpful any time drone data is not looking as clean as the base station data.

The fix I chose was very easy to implement but a better fix would probably have been to wrap just the receiver in a shield rather than placing a shield between the control board and the receiver. This would protect the receiver better and avoid affecting commands sent from the transmitter.  In fact, based on these results, I suspect shielding the GPS receiver on a drone is always a good idea.

New firmware, new satellites, new code

CSG Shop is now shipping all of their M8N and M8T u-blox receivers with the latest version 3 firmware.  This is not such good news for the M8N units since the raw measurements are scrambled and these receivers need to be downgraded to the previous firmware version before using with RTKLIB.  For the M8T receivers though, the new firmware is good news because it contains support for the Galileo satellite system.

I now have two of their M8T receivers with the new firmware and did a little testing to see how RTKLIB works with the Galileo measurements.  I did have to make a couple small changes to get things working.

First of all, the RNX2RTKP compile options for including the Galileo code was not enabled.  For some reason, all the other apps did have this option enabled.  To enable it, I had to add “ENAGAL” to the “Preprocessor Defintions”  for C/C++ in the Project menu in Visual Studio.

The second issue I ran into was in the decode_rxmrawx() function that decodes the raw u-blox RXM-RAWX messages.  There is a line of code in this function that sets the code type based on the system.


This line sets the code to L1X for Galileo, but that code type doesn’t seem to be supported by RTKLIB and the measurements in the RINEX file for the Galileo satellites get left blank.  Changing the “L1X” in the above statement to “L1C” resolves the problem.  That leaves an unnecessary check in the code but I will leave it there at least until I understand what it was supposed to do.  After that everything else worked fine including ambiguity resolution with the Galileo satellites, so that was quite encouraging.

Next,  I put the two receivers outside in the front yard to collect a longer set of data.  Not an ideal environment because they were close to the house but fairly open skies otherwise.  In an hour of data collection I got measurements from 11 GPS satellites, 8 GLONASS satellites, 5 Galileo satellites, and 3 SBAS satellites. After collecting the data, I processed it with various constellation options to see how they compared.  For all the solutions, I set ambiguity resolution mode to “continuous”, position mode to “kinematic”, and opened up the position variance threshold for AR (arthres1) to allow the solution to lock up as early as possible.  I also enabled all constellations for ambiguity resolution in each case.  Here’s how they compared:



Note that the time scale on the GPS-only plot is very different than the others since it took much longer to lock up than any of the other combinations.  With the GPS satellites only, there was an initial short false fix after 14 minutes, then a good fix at 27 minutes that lasted a few minutes but it did not get a solid fix until 43 minutes after it started.  That’s a long time to wait!  Adding a second constellation significantly improved the results, with solid fixes coming after two minutes with GLONASS added, five minutes with SBAS added, and 7 minutes with Galileo added.  Adding a third consellation improved things even more, with times to first solid fix varying from 12 secs for GPS+SBAS+GLO, 3.5 min for GPS+GAL+GLO, and 6 min for GPS+SBAS+GAL.  Using all four constellations gave a time to first solid fix of 2 minutes, not the fastest time, but better than two out of three of the three constellation answers.

It is risky to conclude too much from one data set, but these results are consistent with other data I’ve looked at (for three constellations) that show the more satellites you use the better the answer.  This seems to make sense to me since more information should be better than less information.  However, I often hear or read recommendations to use only the GPS data for better results which I don’t understand.  If anyone has data to support that recommendation I would like to see it to understand it better.

I do sometimes see that one bad satellite can prevent or delay a solution no matter how many good satellites there are and this may be part of the answer.  The more satellites you use, the higher chance there is of having a bad one and RTKLIB is not great at rejecting a bad satellite.  The “arlockcnt” and “ARFilter” features do help prevent bad satellites from getting into the AR solution but they do not reject a satellite if it goes bad after being accepted into the solution.  I have added a new feature starting with the demo5 b26a code that does try to reject bad satellites after they have been accepted into the AR solution but have not had a chance to do a lot of testing on it yet.  It was enabled for the test above and may possibly have helped, I did not look into the details.  The feature is enabled by setting the “pos2-mindropsats” to a value lower than the number of satellites in the solution, in which case it will cycle through dropping all the satellites, one by one, one each epoch, and reject a satellite that has a large negative effect on the AR ratio.  If you try this feature, be careful not to set the minimum satellite threshold too low or you will increase the chances of a false fix.  I would recommend values no lower than 10 satellites.

I have released a new version of the demo 5 code (b26b) with the fixes for Galileo, a couple of new features and fixes, and GUI updates for RTKPOST and RTKNAVI for all the new input parameters for both b26a and b26b codes.  The binaries and a list of the changes are available here.  The source code is available on my Github page.




A fix for the RTCM time tag issue

In my last post I described a problem with a loss of some of the raw measurement information caused by the lack of resolution in the time tags in the RTCM format.  Since the RTCM format is typically used to reduce bandwidth requirements in real-time applications, it is causing real-time solutions to fail when post-processing the same raw data without the translation to RTCM gives good results.  In this post I will describe a fix for this problem.

First of all I want to thank Felipe Nievinski, Igor Vereninov from Emlid, and Anthony Woolridge for their comments to the last post that pointed me to the solution.  They make this a collaborative effort between the U.S., Brazil, Russia, and the U.K!  It still amazes me how enabling the internet can be!

I’ll start by showing again this example of a RINEX output from an M8T receiver with the official raw measurement output (RXM_RAWX) and the debug raw measurement output (TRK_MEAS) enabled simultaneously.  I think  this provides a good insight to what is going on.  The RXM_RAWX message is the top 5 lines and the TRK_MEAS message is the bottom 5 lines for a single epoch.  The first line in each message is the time stamp and the following lines are the measurements for each satellite.  In the satellite measurements, the second column contains the pseduorange value.


The time stamp specifies the receiver time of the received signals and the sixth column is the number of seconds.  For the TRK_MEAS message these values are always aligned to round numbers based on alignment to the sample rate.  For example in this case the measurement rate was 5 Hz and all the time stamps occur on multiples of 0.2.  This is because they are based on the raw receiver clock without any corrections.

The time stamps from the RXM_RAWX messages however often differ from the round numbers by small arbitrary amounts.  This is because the receiver has estimated the error in its own clock and adjusted the measurements to remove this error.  In this case the estimate of clock error is 0.001 seconds and so the time stamp is adjusted by this value (18.8000000 to 18.7990000).

To keep the time stamps consistent with the other parts of the measurement, the clock error also needs to be removed from the psuedorange and carrier phase values since they are based on the difference in time between satellite transmission and receiver reception and will include any errors in the receiver clock.  We see from the above observations that the pseudorange measurement for satellite G24 has been adjusted from 22675327.198 to 22375547.970, a difference of 299779.228 meters.   The speed of light is 299792458 meters per second so the clock error of 0.001 seconds is equivalent to 299792.458 meters,  a value very close to the amount that the pseudorange was adjusted by.

A similar adjustment needs to be made to the carrier phase measurement as well but it is not as easy to see in this example because the carrier phase measurements are relative rather than absolute and the two messages in this case use different references.  The carrier phase measurements are in cycles, not meters, so the frequency of the carrier phase needs to be included in the translation from clock error to carrier phase cycles but is otherwise the same as the pseudorange adjustment.  In equation form, the adjustments are:

P = P -toff*c
L =L – toff*freq

where P=pseudorange, L=carrier phase, c= speed of light, and freq=carrier frequency

So, basically, the receiver is trying to help us out by removing its best estimate of the clock error from the measurements.  This is unnecessary since RTKLIB is quite good at estimating this clock error on its own, but by itself this adjustment does not cause a problem.

It is when the adjusted measurement is translated to RTCM that we get in trouble.  The resolution of the time stamps in the RTCM format is 0.001 seconds.  In this particular example it would not be an issue because the error is exactly 0.001 seconds or one count of the RTCM format.  Most of the time, however, this error is not an exact multiple of 1 millisec.

For example, here is a time stamp for the data set described in the previous posts.

> 2017  1 17 20 31 48.9995584  0  9

And here is the same time stamp after being translated to RTCM and then to RINEX

> 2017  1 17 20 31 49.0000000  0  9

As you can see, the clock adjustment was less than half a millisec so was completely lost in the roundoff to the RTCM format.  However, the adjustments the receiver made to the pseudorange and carrier phase are still present in those measurements.  We now have a problem because the clock correction is in part of the measurement and not the other pieces.  RTKLIB can not correct for this lack of consistency within the measurement.

So, how do we avoid this problem?  Fortunately, RTKLIB has an option to adjust the time stamps to round values using the same equations described above to adjust time stamp, pseudorange, and carrier phase to maintain consistency within the measurement.   I imagine it was put in specifically to solve this problem. We can invoke this option by adding “-TADJ=0.001” in the “Options” box in the “Conversion Options” menu in STRSVR or using the “-opt” option in the command line with STR2STR.  Note that this option needs to be set in the conversion from raw binary format to RTCM format, not the conversion from RTCM to RINEX.  It is possible to set this option when converting from RTCM to RINEX but this won’t help because the damage has already been done in the earlier conversion.

Unfortunately, there is a bug in the implementation of this option in RTKLIB, at least for the u-blox receivers, so by itself, this is not enough.  The problem is that invalid carrier phase measurements are flagged in RTKLIB by setting the carrier phase value to zero.  The time stamp adjustment feature adjusts these zero values slightly so they are no longer recognized as invalid.  They end up getting included in the output as valid measurements and corrupt the solution.

Fortunately, the fix for this bug is very simple.  Here is the code in the decode_rxmrawx() function in ublox.c that makes the adjustment:

/* offset by time tag adjustment */
if (toff!=0.0) {

If we add a check to the first line of code to skip the adjustment if the carrier phase is zero, then all is fine.

if (toff!=0.0&&cp1!=0) {

Below is the original solution after RTCM conversion on the left and with time tag adjustment and the bug fix on the right.  If you compare the solution on the right to the solution with no  RTCM correction in the previous post you will see they are nearly identical.


I am still wary of using RTCM because of its other limitations described in the last  post, particularly the loss of the half cycle invalid flag and the doppler information, but I believe this fix eliminates the most serious issue that comes from using RTCM.

I will release a new version of the demo5 code with this fix sometime in the next few days.  It will take a little while because I also want to include some other features that have been waiting in the pipeline.  If you want to try the fix right away, you just need to  modify the one line of code described above and rebuild.

Update 2/2/17:    I have taken Anthony Woolridge’s suggestion and modified the RTCM conversion code to automatically adjust the pseudorange and carrier phase measurements to compensate for any round off done to the time tag.  This means it is not necessary to set the time-tag adjust receiver option.  This change is currently checked into my Github page and I hope to post new executables in the next couple of days.

Moving-base solutions (part 2)

In my last post I discussed solving for moving-base data sets using the ordinary fixed-base solution modes and promised to discuss solutions using the RTKLIB “movingbase” method in my next post.

Let me start by saying I had hoped to have had more success with this method by the time I got to writing this post but that has not been the case.  I have tried both the most recent demo5 code and the 2.4.3 release code and neither gives me clean reliable solutions if I turn on the “movingbase” option.

In the previous post I had picked a fairly challenging data set to demonstrate with.  In case that was interfering with the solution, I first switched to a cleaner data set for this experiment.  This is a data set taken with two Emlid Reach M8T receivers, one mounted on each end of a kayak while out in the ocean near Sussex England and was sent to me by Matt. Here is the solution using the same input configuration file I used in my previous post, with “movingbase” turned off.


The distance between the receivers in this case is larger and the deviations from a circle are very small.  This result should provide very accurate heading measurements.  The two visible deviations from the circle in the plot above are caused by rolling the kayak over an embankment at at launch and retrieval.  These large z-axis movements violate the assumption that movements are all in the x-y direction and cause the solution to leave the circle onto a sphere but are not actual errors.

Here’s a solution using the latest 2.4.3 code with “movingbase” enabled.


It may be that I am doing something silly but I did spend a fair bit of time trying to get a decent solution without success.  If anybody more familiar with “movingbase”solutions would like to take a shot at it, I’ve uploaded this data set to here on the rtkexplorer.com website.  Please let me know if you are able to get a decent “movingbase” solution with this data.

I went back to the more challenging original data set from last post since I actually had slightly more success with that one, although still quite limited.

In my first attempt with “movingbase” enabled, I ran into the same problem as last post where the missing measurements in the base data caused large spikes in the solution.  This was true even with the max age of differential set to less than one sample time, which is what fixed the problem previously.  Looking at the code, this is because the “maxage” input parameter is ignored when “movingbase” is set and a hard-coded value is used instead (more about this below).   I modified the code so that it did check the “maxage” limit for “movingbase” and then got the following solution.


The spikes are much smaller now that the missing samples are removed but they are still occurring, this time when both measurements are present.  The spikes are large enough to make this solution useless.  At this point I have given up trying to get useful results with the “movingbase” solution but again would be very interested if someone else can show good results for this data as well.  The raw data is located at the same link as the previous data set.

I am not completely surprised that the “movingbase” solutions are not working well, since the only other case I’m aware of that RTKLIB allows the base to move also has caused me problems.  That occurs when running real-time solutions and setting the base location to “Average of Single Positions” and then setting the number of averages greater than one.   Whenever I have done this, the solution takes a long time to converge.  I get much faster first fixes if I set the number of averages to one which then prevents the base location moving after the first measurement.

Since I did spend some time going through the code to understand how the “movingbase” solution is supposed to work, I thought I would share that here.  Setting the solution mode to “movingbase” sets the opt->mode variable in the code to “PMODE_MOVEB” so I started by searching the code base for this.  There is also a section in the RTKLIB manual in Appendix E that describes the moving-base model.  Here’s a quick summary for the significant differences I found that occur when in “movingbase” mode

  1. Adjust base position every epoch: Based on single point position result.
  2.  Synchronize rover/base measurements:  The measurement times between the two receivers may vary slightly (usually less than 2 msec).  This can degrade accuracy in the case of very fast-moving rovers.  To prevent this, the base measurements are adjusted for their time difference.  Uses a hard-coded value (1.01 sec) for max age of differential instead of the “maxage”input config parameter.
  3. Constrain baseline: Add a pseudo-measurement to the kalman filter measurement update based on the error from the baseline length specified in “pos2-baselen” and “pos2-basesig” input parameters. (Only applied if pos2-baselen>0)
  4. Increase kalman filter update iterations:  Add two iterations to the number of iterations specified by the “pos2-niter” input parameter.  This should improve the response in the case of large non-linearities introduced by short baselines or rotational accelerations.

So, based on these results, my recommendation for processing moving-base data is to use the ordinary fixed-base solution parameters I described in my previous post.  This will usually give good results but be aware that there will be limitations in the cases where the rover moves a very long distance away from it’s starting point or if is moving fast relative to any sampling time deviations between the two rovers.



Exploring moving-base solutions

Recently, I’ve had several questions about moving-base solutions so that will be the topic for this post.

As you might guess from the name, a moving-base solution differs from the more common fixed-based solutions in that the base station is allowed to move in addition to the rover.   Although it could be used to track the distance between two moving rovers it is more commonly used in a configuration with two receivers attached to a single rover and used to determine heading. Since the receivers remain at a fixed distance from each other, the solution in this case becomes a circle with a radius equal to the distance between the receivers.  The location on this circle corresponds to the rover’s heading which is easily calculated using a four quadrant arctan of the x and y components of the position.  I also used moving base solutions in several of my earliest posts because the circular nature of the solution makes it easier to verify the solution and to measure errors.  Since all solution points should be on the circle, any deviation from the circle can be assumed to be error.

To be more exact, everywhere I mention “circle” above I really should say “sphere” instead since the solution has three dimensions, but if the rover is ground-based, the movements in the z-axis will be relatively small and for simplicity we can assume it is a circle.

In fixed-base solutions, the measurement rate of the base station is often lower than the rover both because it’s location is not changing and also because the base data often has to be transmitted over a data link which may be bandwidth limited.  In a moving-base solution, since both receivers are moving, and there is usually no need for a data link since they are both attached to the same rover, it makes sense to use the same data rate for both receivers.

For this exercise, I chose to use a data set I discussed previously in my “M8N vs M8T” series of posts.  It consists of two receivers, an M8N and an M8T, on top of a moving car and another M8T receiver used as a fixed base station.  The car drives on roads with a fairly open sky view for up to a couple kilometers away from the base station.  The base station is located next to some sheds and a tree, so is not ideal, but still has fairly open skies.  All three receivers  ran at 5 Hz sampling rate and both moving receivers have some missing samples.  I’m not sure exactly why this is, it may be because I used a single laptop to collect both data streams.  Regardless of where they come from, I have found occasional missing samples are fairly common whenever I collect data at higher sample rates and believe the solution should be robust enough to handle them.  The rover M8T data also has a simultaneous cycle-slip type receiver glitch near the beginning of the data as described in my last post.  Overall, I would consider this a moderately challenging data set but those are often the best kind for testing the limits of RTKLIB.


Having data from three receivers gives us the luxury of being able to calculate three different solutions (base->rover1, base->rover2, and rover1->rover2) and then compare results between them.  Since the first two solutions are fixed-base and the third is a moving-base, it also allows us to validate the moving-base solution using a combination of the two fixed-base solutions.

To start with, let’s calculate solutions for the distance between each moving receiver relative to the fixed base station using the demo5 code and my standard config files for the M8N and M8T receivers.  The only difference between the two config files is that the GLONASS ambiguity resolution (gloarmode) is set to “fix-and-hold” for the M8N config file and to “on” for the M8T config file for reasons explained in previous posts.   I’ve also done the conversion from raw data to RINEX observation files with the TRK_MEAS and STD_SLIP receiver options set to 2 and 4 respectively, again for reasons previously explained.  I set the solution mode to “static-start” since I knew the data set started with the rover stationary for long enough to get a first fix but I also could have used “kinematic” mode.

Subtracting the two fixed-base solutions gives us the distance between the two rover receivers which should be equal to a moving-base solution calculated directly between the two rovers.  The only difference is that the errors will be larger in the difference of two solutions than they will be in the direct solution because the errors in the combined solutions will accumulate.

Here are the positions and ground track for the difference between the two solutions, using the “1-2” plotting option in RTKPLOT.  As expected we get a circle for the ground track.  From the radius of the circle we can tell that the two rovers were about 15 cm apart.  Usually you would put the two receivers as far apart as possible, since the errors in the heading will decrease as the distance between the rovers increases but in this case I hadn’t intended to use the results this way so had placed the rovers closer together.  Still, it might be representative of a configuration on a small drone or other small rover.


Next let’s try to calculate the solution directly between the two moving receivers.  RTKLIB does have a special “moving-base” mode but we won’t use this yet.  The “kinematic” solution calculates the distance between the two rovers regardless of the location of the base, so for now we can ignore the fact that the base is moving.  This will breakdown eventually if the rover gets too far from the base but since in this data set the rover is only a couple kilometers from the base at its farthest point we should be OK.

The only change I made to the config file from the previous M8N run for this run was to reduce the acceleration input parameters “stats-prnaccelh” and “stats-prnaccelv” which are used to describe the acceleration characteristics of the rover in the horizontal and vertical directions relative to the base.  In the fixed-base solution, these need to include both the linear accelerations and rotational accelerations since the rover is moving and the base is fixed, but in the moving-base solution, since we care only about differential acceleration between the receivers, we can ignore the linear accelerations and include only the rotational accelerations.  I just used a rough guess and reduced the numbers from (1,0.25) to (0.25,0.1) but I could have found more exact numbers by looking at the acceleration plot of an initial run of the solution.

Here’s the solution using this configuration.  It looks reasonable except for the occasional large spikes.


After a little debugging, I found that the spikes were occurring wherever there was a missing sample in the base data.  When this occurs, RTKLIB just uses the previous base sample.  This works fine when the base is not moving, but in this case that’s no longer true, and the previous base measurements are not good estimates of the current position.  We can tell RTKLIB to skip these measurements by setting the maximum age of differential to something less than one sample time.  This is done with the “pos2-maxage” input parameter.  I set it to 0.1 which is half of one sample time.

With this change, I got the following solution for the position.  Much better!


The ground track for this solution is shown below on the right, on the left is the previous ground track derived by subtracting the two fixed-base solutions.  As expected, the solutions look very similar except the moving-base solution has smaller errors which appear as deviations from a perfect circle.


To further validate this solution we can compare the heading calculated from the moving-base position with the heading determined from the velocity vector of the fixed-base solution.  This wouldn’t work if the rover were a boat, drone, or person, but in the case of a car there are no external lateral drifts and the car will move in the direction it is pointed (unless it’s in reverse of course).   This won’t work if the velocity is zero or near zero but for reasonably high velocities we should get a good match.  The top plot below shows the difference between the two.  The blue line is for all velocities and the red is for when the velocity drops below 5 m/s.  The bottom plot shows the distance from the base to the rover.


As expected, the errors are large when the velocities are low but we get a good match otherwise.  There also appears to be no correlation between the errors and the base to rover distance which suggests we are well below the maximum base to rover distance before we start to see issues with our assumption that the base did not move.

Overall, this solution looks excellent, with 100% fix and based on deviations from the circle, very small errors.  In fact, I recommend this configuration over the RTKLIB “moving-base” solution if you are able to live within the maximum baseline constraints.  I don’t know how large that is, but it looks like it may be significantly larger than 2 kilometers which is probably large enough for most applications.

In the next post I will explore what happens when the RTKLIB solution mode is set to “movingbase” in more detail but for now let me bring up just one of its effects since it is something we can also do here without invoking “movingbase” mode and it may have some benefit.

RTKLIB uses an extended kalman filter which is designed to handle non-linearities in the system by linearizing around the current operating point.  This generally works quite well but as the system becomes more non-linear, the errors introduced by this approximation grow larger.  One way to deal with this is to run multiple iterations of the kalman filter every measurement sample to converge on the correct answer.  As we get closer to the correct answer, we will operate closer to the point around which the system has been linearized and the errors will be smaller.  There is an input parameter in RTKLIB called “pos2-niter” that specifies the number of filter iterations for each sample.  The default value is one but when “posmode” is set to “movingbase” two iterations are automatically added to whatever this value is set to.  In the default case, we would get three iterations every sample instead of one.  Since the kalman filter assumes all velocities are linear and in the moving-base case we have been looking at, they are all rotational and non-linear, it might make sense to do this.  In my example, the sample rate is quite high relative to the rate of rotation and I found it did not help, but in other cases where the rate of rotation is higher relative to the sample rate, it might be a good idea.

So, let me finish by summarizing the changes I recommend for moving-base solutions.

  1.  Set measurement sample rate for both rover and base to the same value
  2. Leave “pos1-posmode” set to “kinematic” or “static-start”
  3. Set “pos2-maxage” to half the sample time (e.g 0.1 for 5 samples/sec)
  4. Reduce “stats-prnaccelh” and “stats-prnaccelv” to reflect differential accelearation
  5. Experiment with increasing “pos2-niter” from 1 to 3

These recommendations are based on my fairly limited experience with moving-base solutions so if anybody else has other recommendations, please respond in the Comments section.

I have added the data set I used here to the data sets available for download on rtkexplorer.com for anyone who would like to experiment further with this data.

In the next post, I will talk more about what happens when “pos1-posmode” is set to “movingbase”.