DOP

Dillution of Precision (DOP) describes the effect of satellite geometry on the user’s positioning accuracy. DOP can refer to multiple ideas:

  1. DOP in the horizontal or vertical direction,

  2. DOP on the time uncertainty,

  3. The full DOP matrix, or

  4. DOP in a particular direction.

Here we show how to make use of existing functionality. For more details on the underlying math, please see the Navipedia page on positioning error.

[1]:
import gnss_lib_py as glp
import numpy as np

# A library for url downloads that works regardless of `wget` command
import urllib.request

As an example, we can load in data from the 2022 Google Smartphone Decimeter Challenge

[2]:
glp.make_dir("../data")
urllib.request.urlretrieve("https://raw.githubusercontent.com/Stanford-NavLab/gnss_lib_py/main/data/unit_test/google_decimeter_2022/device_gnss.csv", "../data/device_gnss.csv")

navdata = glp.AndroidDerived2022("../data/device_gnss.csv")

Since we already have elevation and azimuth data available to us, we can simply call the get_dop function, which will only return the HDOP and VDOP values, by default. HDOP corresponds to the dilution of precision in the horizontal direction (i.e., East and North plane), and VDOP corresponds to the dilution of precision in the vertical direction (i.e., the Up axis).

[3]:
dop_navdata = glp.get_dop(navdata)
print(dop_navdata)
     gps_millis      HDOP      VDOP
0  1.303771e+12  0.558578  0.830517
1  1.303771e+12  0.549160  0.829362
2  1.303771e+12  0.558593  0.830452
3  1.303771e+12  0.549176  0.829296
4  1.303771e+12  0.549185  0.829263
5  1.303771e+12  0.549193  0.829230

Some applications may care about the dilution of precision in time (TDOP), but may not be interested in the dilution of precision in position. Simply pass this information to the parser.

[4]:
dop_navdata = glp.get_dop(navdata, HDOP=False, VDOP=False, TDOP=True)
print(dop_navdata)
     gps_millis      TDOP
0  1.303771e+12  0.533824
1  1.303771e+12  0.529356
2  1.303771e+12  0.533763
3  1.303771e+12  0.529294
4  1.303771e+12  0.529263
5  1.303771e+12  0.529233

Below we illustrate all supported DOP types. The full DOP matrix (in ENU) is

\[\begin{split}Q = \begin{bmatrix} q_{ee} & q_{en} & q_{eu} & q_{et} \\ q_{ne} & q_{nn} & q_{nu} & q_{nt} \\ q_{ue} & q_{un} & q_{uu} & q_{ut} \\ q_{te} & q_{tn} & q_{tu} & q_{tt} \\ \end{bmatrix}\end{split}\]

The matrix is symmetric (i.e., \(q_{en} = q_{ne}\)). Often the elements along the diagonal are of primary interest

\[\begin{split}Q = \begin{bmatrix} \text{EDOP}^2 & \cdot & \cdot & \cdot \\ \cdot & \text{NDOP}^2 & \cdot & \cdot \\ \cdot & \cdot & \text{VDOP}^2 & \cdot \\ \cdot & \cdot & \cdot & \text{TDOP}^2 \end{bmatrix}\end{split}\]

To store the dop matrix \(Q\) in dop_navdata, the upper triangle is splatted across columns to enable fast storage and access in the navdata using numpy.

[5]:
dop_navdata = glp.get_dop(navdata, GDOP= True, HDOP=True, VDOP=True,
                          PDOP=True, TDOP=True, dop_matrix=True)
print(dop_navdata)
     gps_millis      GDOP      HDOP      VDOP      PDOP      TDOP    dop_ee  \
0  1.303771e+12  1.134343  0.558578  0.830517  1.000883  0.533824  0.163043
1  1.303771e+12  1.126781  0.549160  0.829362  0.994695  0.529356  0.157412
2  1.303771e+12  1.134275  0.558593  0.830452  1.000838  0.533763  0.163059
3  1.303771e+12  1.126711  0.549176  0.829296  0.994649  0.529294  0.157430
4  1.303771e+12  1.126677  0.549185  0.829263  0.994626  0.529263  0.157439
5  1.303771e+12  1.126642  0.549193  0.829230  0.994603  0.529233  0.157448

     dop_en    dop_eu    dop_et    dop_nn    dop_nu    dop_nt    dop_uu  \
0 -0.033487  0.071130 -0.034933  0.148966 -0.107539  0.065920  0.689758
1 -0.038702  0.074427 -0.040130  0.144165 -0.104554  0.061164  0.687841
2 -0.033509  0.071241 -0.035010  0.148967 -0.107550  0.065927  0.689650
3 -0.038724  0.074539 -0.040206  0.144165 -0.104563  0.061170  0.687732
4 -0.038735  0.074596 -0.040245  0.144165 -0.104567  0.061173  0.687677
5 -0.038745  0.074652 -0.040284  0.144165 -0.104572  0.061177  0.687623

     dop_ut    dop_tt
0 -0.410704  0.284968
1 -0.407697  0.280218
2 -0.410618  0.284903
3 -0.407610  0.280152
4 -0.407566  0.280120
5 -0.407523  0.280087

We can recover the unsplatted versions of the DOP matrix as needed if we loop through time.

[6]:
for timestamp, _, dop_navdata_subset in glp.loop_time(dop_navdata, 'gps_millis'):

    labels = glp.get_enu_dop_labels()

    dop_matrix_splat = np.array(
        [dop_navdata_subset[f'dop_{label}'] for label in labels])

    print(f"At time {timestamp} the DOP matrix is")
    print(glp.unsplat_dop_matrix(dop_matrix_splat))
At time 1303770943999.0 the DOP matrix is
[[ 0.16304289 -0.03348691  0.07112954 -0.03493291]
 [-0.03348691  0.14896632 -0.10753946  0.0659197 ]
 [ 0.07112954 -0.10753946  0.68975786 -0.4107043 ]
 [-0.03493291  0.0659197  -0.4107043   0.28496798]]
At time 1303770944999.0 the DOP matrix is
[[ 0.15741177 -0.03870195  0.07442678 -0.04012966]
 [-0.03870195  0.14416461 -0.10455384  0.06116369]
 [ 0.07442678 -0.10455384  0.68784096 -0.40769683]
 [-0.04012966  0.06116369 -0.40769683  0.28021779]]
At time 1303770945999.0 the DOP matrix is
[[ 0.16305935 -0.03350896  0.07124139 -0.03501028]
 [-0.03350896  0.14896727 -0.10754991  0.06592695]
 [ 0.07124139 -0.10754991  0.68964999 -0.41061842]
 [-0.03501028  0.06592695 -0.41061842  0.28490285]]
At time 1303770946999.0 the DOP matrix is
[[ 0.15742999 -0.03872364  0.07453933 -0.04020645]
 [-0.03872364  0.14416459 -0.10456265  0.0611701 ]
 [ 0.07453933 -0.10456265  0.68773184 -0.40760979]
 [-0.04020645  0.0611701  -0.40760979  0.28015229]]
At time 1303770947999.0 the DOP matrix is
[[ 0.15743916 -0.03873452  0.07459567 -0.0402449 ]
 [-0.03873452  0.14416463 -0.10456715  0.06117339]
 [ 0.07459567 -0.10456715  0.68767735 -0.4075664 ]
 [-0.0402449   0.06117339 -0.4075664   0.28011966]]
At time 1303770948999.0 the DOP matrix is
[[ 0.15744842 -0.03874549  0.07465221 -0.04028354]
 [-0.03874549  0.14416482 -0.10457202  0.061177  ]
 [ 0.07465221 -0.10457202  0.68762276 -0.40752307]
 [-0.04028354  0.061177   -0.40752307  0.28008714]]

Lastly, we can compute the contribution in a particular direction with numpy.

[7]:
direction_of_interest = np.array([-1, 1, 0, 0])
# Normalize the direction of interest
direction_of_interest = direction_of_interest / np.linalg.norm(direction_of_interest)

for timestamp, _, dop_navdata_subset in glp.loop_time(dop_navdata, 'gps_millis'):

    labels = glp.get_enu_dop_labels()

    dop_matrix_splat = np.array(
        [dop_navdata_subset[f'dop_{label}'] for label in labels])
    dop_matrix_unsplat = glp.unsplat_dop_matrix(dop_matrix_splat)

    dop_in_direction = np.sqrt(
        direction_of_interest @ dop_matrix_unsplat @ direction_of_interest)

    print(f"At time {timestamp} the DOP in the direction of interest is {dop_in_direction}")
At time 1303770943999.0 the DOP in the direction of interest is 0.43530623907154725
At time 1303770944999.0 the DOP in the direction of interest is 0.43530464856176754
At time 1303770945999.0 the DOP in the direction of interest is 0.43534155075325415
At time 1303770946999.0 the DOP in the direction of interest is 0.43534001360708996
At time 1303770947999.0 the DOP in the direction of interest is 0.4353578004877099
At time 1303770948999.0 the DOP in the direction of interest is 0.43537583394008916