|
11 | 11 | "## Overview\n", |
12 | 12 | "Differential GNSS (DGNSS) improves positioning accuracy by using a reference receiver at a precisely known location to correct errors affecting nearby GNSS users. The reference station receives the same satellite signals as the user and compares the measured ranges to the true ranges based on its known position. The differences represent combined errors from satellite clocks, orbits, and atmospheric delays. These corrections are transmitted to the user receiver, which applies them to its own measurements. Because both receivers experience nearly the same errors, especially when close together, many of these errors cancel out, reducing position errors from several meters to sub-meter or better accuracy.\n", |
13 | 13 | "\n", |
14 | | - "GNSS positioning problems can be encoded as factor graphs in GTSAM; this notebook extends the groundwork from [SinglePointPositioningExample.ipynb](https://github.com/borglab/gtsam/blob/develop/python/gtsam/examples/SinglePointPositioningExample.ipynb) to improve receiver positioning accuracy using corrections from a nearby reference receiver." |
| 14 | + "GNSS positioning problems can be encoded as factor graphs in GTSAM; this notebook extends the groundwork from [SinglePointPositioningExample.ipynb](https://borglab.github.io/gtsam/singlepointpositioningexample/) to improve receiver positioning accuracy using corrections from a nearby reference receiver." |
15 | 15 | ] |
16 | 16 | }, |
17 | 17 | { |
|
43 | 43 | "source": [ |
44 | 44 | "try:\n", |
45 | 45 | " import google.colab\n", |
46 | | - " %pip install --quiet pyrtklib numpy gtsam-develop pyproj tabulate\n", |
| 46 | + " %pip install --quiet pyrtklib numpy gtsam-develop pyproj tabulate folium\n", |
| 47 | + " !wget https://raw.githubusercontent.com/borglab/gtsam/refs/heads/develop/python/gtsam/examples/gnss_utils.py\n", |
47 | 48 | "except ImportError:\n", |
48 | 49 | " pass\n", |
49 | 50 | "\n", |
| 51 | + "import folium\n", |
50 | 52 | "import gnss_utils\n", |
51 | 53 | "import gtsam\n", |
52 | 54 | "from gtsam.symbol_shorthand import B, C, X\n", |
|
165 | 167 | "cell_type": "markdown", |
166 | 168 | "metadata": {}, |
167 | 169 | "source": [ |
168 | | - "So far the solution passes the eye-ball test: It's in the right US state, in roughly the correct part of the SF Bay Area. But we can quantify error by comparing the [actual position of P222](https://www.ngs.noaa.gov/cgi-cors/CorsSidebarSelect.prl?site=P222&option=Coordinates20):\n", |
| 170 | + "So far this solution passes the eye-ball test: It's in the right US state, in roughly the correct part of the SF Bay Area. But we can quantify error by comparing the [actual position of P222](https://www.ngs.noaa.gov/cgi-cors/CorsSidebarSelect.prl?site=P222&option=Coordinates20):\n", |
169 | 171 | "```\n", |
170 | 172 | "| ITRF2020 POSITION (EPOCH 2020.0) |\n", |
171 | 173 | "| Computed in Apr 2025 using data through gpswk 2237. |\n", |
|
204 | 206 | "```\n", |
205 | 207 | "In a factor graph framework, these survey coordinates correspond to a stiff `PriorFactor` constraint on ZOA1's position node to keep it anchored in the global Earth frame. The rest of the factor graph is constructed as follows:\n", |
206 | 208 | "\n", |
207 | | - "```mermaid\n", |
208 | | - "graph TD\n", |
209 | | - " %% Variable nodes\n", |
210 | | - " c1((Differential Correction))\n", |
211 | | - " \n", |
212 | | - " subgraph P222\n", |
213 | | - " direction LR\n", |
214 | | - " x1((P222 Position))\n", |
215 | | - " b1((P222 Clock Bias))\n", |
216 | | - " end\n", |
217 | | - "\n", |
218 | | - " subgraph ZOA1\n", |
219 | | - " direction LR\n", |
220 | | - " x2((ZOA1 Position))\n", |
221 | | - " b2((ZOA1 Clock Bias))\n", |
222 | | - " end\n", |
223 | | - "\n", |
224 | | - " %% Factor nodes\n", |
225 | | - " p1[PriorFactor]\n", |
226 | | - " dpr1[DifferentialPseudorangeFactor]\n", |
227 | | - " dpr2[DifferentialPseudorangeFactor]\n", |
228 | | - "\n", |
229 | | - " %% Connect ZOA1's prior\n", |
230 | | - " p1 --- x2\n", |
| 209 | + "\n", |
231 | 210 | "\n", |
232 | | - " %% Connect ZOA1's pseudorange\n", |
233 | | - " dpr1 --- c1\n", |
234 | | - " dpr1 --- x2\n", |
235 | | - " dpr1 --- b2\n", |
236 | | - "\n", |
237 | | - " %% Connect P222's pseudorange\n", |
238 | | - " dpr2 --- c1\n", |
239 | | - " dpr2 --- x1\n", |
240 | | - " dpr2 --- b1\n", |
241 | | - "```\n", |
242 | | - "\n", |
243 | | - "The bottom row are variable nodes, and the top row are factors. A `DifferentialPseudorangeFactor` is added for each receiver observation, and their correction nodes are indexed by satellite ID. For example, ZOA1's pseudorange factor of satellite 25 links to the same correction node for P222's pseudorange observation of satellite 25. Since different satellites are in different positions in the sky, each satellite has their own correction node to account for spatial variation in atmospheric effects. Furthermore, for long-time-series observations, the correction nodes must also account for temporal variation in the atmosphere. In that case, a linear chain of correction nodes linked with between-factors is recommended. The above structure is encoded into GTSAM below:" |
| 211 | + "A `DifferentialPseudorangeFactor` is added for each receiver observation, and their correction nodes are indexed by satellite ID. For example, ZOA1's pseudorange factor of satellite 25 links to the same correction node for P222's pseudorange observation of satellite 25. Since different satellites are in different positions in the sky, each satellite has their own correction node to account for spatial variation in atmospheric effects. Furthermore, for long-time-series observations, the correction nodes must also account for temporal variation in the atmosphere. In that case, a linear chain of correction nodes linked with between-factors is recommended. The above structure is encoded into GTSAM below:" |
244 | 212 | ] |
245 | 213 | }, |
246 | 214 | { |
|
305 | 273 | "\n", |
306 | 274 | "# Process the results:\n", |
307 | 275 | "clock_bias = result.atDouble(p222_bias_key)\n", |
308 | | - "receiver_position = result.atVector(p222_pos_key)\n", |
309 | | - "\n", |
| 276 | + "corrected_receiver_position = result.atVector(p222_pos_key)\n", |
| 277 | + "print(f\"Corrected P222 position: {corrected_receiver_position} ECEF meters\")" |
| 278 | + ] |
| 279 | + }, |
| 280 | + { |
| 281 | + "cell_type": "markdown", |
| 282 | + "metadata": {}, |
| 283 | + "source": [ |
| 284 | + "Let's plot these coordinates on a map, including the reference stations and single-point positions, to get a big-picture overview of how the positioning results stack up:" |
| 285 | + ] |
| 286 | + }, |
| 287 | + { |
| 288 | + "cell_type": "code", |
| 289 | + "execution_count": null, |
| 290 | + "metadata": {}, |
| 291 | + "outputs": [], |
| 292 | + "source": [ |
310 | 293 | "# Convenience plot:\n", |
| 294 | + "lon, lat, alt = ecef2lla.transform(corrected_receiver_position[0], corrected_receiver_position[1], corrected_receiver_position[2])\n", |
| 295 | + "print(f\"Corrected geodetic coordinates: {lat}, {lon}, {alt} m\")\n", |
| 296 | + "\n", |
| 297 | + "# Center the map on P222\n", |
| 298 | + "m = folium.Map(\n", |
| 299 | + " location=[lat, lon],\n", |
| 300 | + " zoom_start=17\n", |
| 301 | + ")\n", |
| 302 | + "\n", |
| 303 | + "# Plot surveyed P222 position:\n", |
| 304 | + "folium.Circle(\n", |
| 305 | + " location=[37.5392407028, -122.0832682528],\n", |
| 306 | + " radius=10,\n", |
| 307 | + " tooltip=\"P222 Truth\",\n", |
| 308 | + " popup=\"Actual P222 position\"\n", |
| 309 | + ").add_to(m)\n", |
| 310 | + "\n", |
| 311 | + "# Plot the corrected receiver position:\n", |
| 312 | + "folium.Marker(\n", |
| 313 | + " location=[lat, lon],\n", |
| 314 | + " tooltip=\"P222 Corrected\",\n", |
| 315 | + " popup=\"Estimated P222 position using corrections from ZOA1\",\n", |
| 316 | + " icon=folium.Icon(color=\"green\", icon=\"plus\")\n", |
| 317 | + ").add_to(m)\n", |
| 318 | + "\n", |
| 319 | + "# Plot the single-point-positioning solution:\n", |
311 | 320 | "lon, lat, alt = ecef2lla.transform(receiver_position[0], receiver_position[1], receiver_position[2])\n", |
312 | | - "print(f\"geodetic: {lat}, {lon}, {alt} m\")\n", |
313 | | - "gnss_utils.plotMap(lat, lon)" |
| 321 | + "folium.Marker(\n", |
| 322 | + " location=[lat, lon],\n", |
| 323 | + " tooltip=\"P222 SPP\",\n", |
| 324 | + " popup=\"Estimated P222 position without differential corrections\",\n", |
| 325 | + " icon=folium.Icon(color=\"orange\", icon=\"minus\")\n", |
| 326 | + ").add_to(m)\n", |
| 327 | + "\n", |
| 328 | + "# Plot ZOA1's position:\n", |
| 329 | + "folium.Marker(\n", |
| 330 | + " location=[37.5430544917, -122.0159500139],\n", |
| 331 | + " tooltip=\"ZOA1\",\n", |
| 332 | + " popup=\"ZOA1 reference station surveyed position\",\n", |
| 333 | + " icon=folium.Icon(color=\"blue\")\n", |
| 334 | + ").add_to(m)\n", |
| 335 | + "\n", |
| 336 | + "m" |
314 | 337 | ] |
315 | 338 | }, |
316 | 339 | { |
317 | 340 | "cell_type": "markdown", |
318 | 341 | "metadata": {}, |
319 | 342 | "source": [ |
320 | | - "We got an updated position; so let's see if it's closer to ground truth?" |
| 343 | + "Scroll and zoom the map to get an idea for the relative positioning error between the single-point and differential solutions. The blue circle indicates the ground-truth surveyed postion of P222. The green \"plus\" marker shows the differentially-corrected solution. The orange \"minus\" marker indicates the uncorrected position. Zoom out further on the map to make the blue ZOA1 receiver position visible.\n", |
| 344 | + "\n", |
| 345 | + "Let's see if it's closer to ground truth?" |
321 | 346 | ] |
322 | 347 | }, |
323 | 348 | { |
|
327 | 352 | "outputs": [], |
328 | 353 | "source": [ |
329 | 354 | "print(f\"ZOA1 position adjustment: {1e3*np.linalg.norm(result.atVector(zoa1_pos_key) - zoa1_ref_pos)} millimeters\")\n", |
330 | | - "error = ground_truth - receiver_position\n", |
| 355 | + "error = ground_truth - corrected_receiver_position\n", |
331 | 356 | "print(f\"Total positioning error: {np.linalg.norm(error)} meters\")" |
332 | 357 | ] |
333 | 358 | }, |
|
347 | 372 | "- [PseudorangeFactor.h](https://github.com/borglab/gtsam/blob/develop/gtsam/navigation/PseudorangeFactor.h)\n", |
348 | 373 | "- [PseudorangeFactor.cpp](https://github.com/borglab/gtsam/blob/develop/gtsam/navigation/PseudorangeFactor.cpp)\n", |
349 | 374 | "- [PseudorangeFactor.ipynb](https://github.com/borglab/gtsam/blob/develop/gtsam/navigation/doc/PseudorangeFactor.ipynb)\n", |
350 | | - "- [SinglePointPositioningExample.ipynb](https://github.com/masoug/gtsam-gnss/blob/develop/python/gtsam/examples/SinglePointPositioningExample.ipynb)" |
| 375 | + "- [SinglePointPositioningExample.ipynb](https://borglab.github.io/gtsam/singlepointpositioningexample/)" |
351 | 376 | ] |
352 | 377 | } |
353 | 378 | ], |
|
0 commit comments