INS/GNSS Navigation

This example demonstrates Inertial Navigation System (INS) and GNSS integration.

Overview

INS/GNSS integration combines:

  • INS: High-rate, smooth navigation with drift

  • GNSS: Accurate but noisy absolute position

  • Integration: Best of both systems

Key Concepts

  • Strapdown mechanization: Integrating IMU measurements

  • Error state: Modeling INS drift

  • Loosely coupled: GNSS position updates

  • Tightly coupled: GNSS pseudorange updates

INS Mechanization

Strapdown INS integrates:

  1. Accelerometers: Specific force measurements

  2. Gyroscopes: Angular rate measurements

  3. Attitude update: Quaternion integration

  4. Velocity update: Transform and integrate acceleration

  5. Position update: Integrate velocity

Error Sources

  • Gyro bias: Causes heading drift

  • Accelerometer bias: Causes position drift

  • Scale factor errors: Proportional errors

  • Coning/sculling: Integration errors

Code Highlights

The example demonstrates:

  • INS state initialization with INSState

  • Strapdown mechanization with mechanize_ins_ned()

  • GNSS update with Kalman filter

  • Error state estimation and correction

  • Trajectory visualization

Source Code

  1"""
  2INS/GNSS Navigation Example.
  3
  4This example demonstrates:
  51. INS mechanization (strapdown navigation)
  62. IMU data processing with coning/sculling corrections
  73. Loosely-coupled INS/GNSS integration
  84. Tightly-coupled INS/GNSS integration
  95. DOP computation and GNSS outage detection
 10
 11Run with: python examples/ins_gnss_navigation.py
 12"""
 13
 14import sys
 15from pathlib import Path
 16
 17sys.path.insert(0, str(Path(__file__).parent.parent))
 18
 19import os
 20
 21import numpy as np  # noqa: E402
 22import plotly.graph_objects as go  # noqa: E402
 23from plotly.subplots import make_subplots  # noqa: E402
 24
 25from pytcl.navigation import (  # noqa: E402
 26    WGS84,
 27    GNSSMeasurement,
 28    IMUData,
 29    coarse_alignment,
 30    compute_dop,
 31    coning_correction,
 32    earth_rate_ned,
 33    geodetic_to_ecef,
 34    gnss_outage_detection,
 35    gravity_ned,
 36    initialize_ins_gnss,
 37    initialize_ins_state,
 38    loose_coupled_predict,
 39    loose_coupled_update,
 40    mechanize_ins_ned,
 41    normal_gravity,
 42    radii_of_curvature,
 43    satellite_elevation_azimuth,
 44    sculling_correction,
 45    transport_rate_ned,
 46)
 47
 48SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 49OUTPUT_DIR = Path(__file__).resolve().parent / "output"
 50
 51
 52def ins_basics_demo() -> None:
 53    """Demonstrate basic INS concepts."""
 54    print("=" * 60)
 55    print("1. INS FUNDAMENTALS")
 56    print("=" * 60)
 57
 58    # Define a location (San Francisco)
 59    lat = np.radians(37.7749)
 60    lon = np.radians(-122.4194)
 61    alt = 100.0  # meters
 62
 63    print("\nLocation: San Francisco")
 64    print(f"  Latitude: {np.degrees(lat):.4f} deg")
 65    print(f"  Longitude: {np.degrees(lon):.4f} deg")
 66    print(f"  Altitude: {alt:.1f} m")
 67
 68    # Normal gravity
 69    g = normal_gravity(lat, alt)
 70    print(f"\nNormal gravity: {g:.6f} m/s^2")
 71
 72    # Gravity in NED frame
 73    g_ned = gravity_ned(lat, alt)
 74    print(
 75        f"Gravity vector (NED): [{g_ned[0]:.6f}, {g_ned[1]:.6f}, {g_ned[2]:.6f}] m/s^2"
 76    )
 77
 78    # Earth rate in NED
 79    omega_ie = earth_rate_ned(lat)
 80    print("\nEarth rotation (NED):")
 81    print(f"  North: {omega_ie[0] * 1e6:.3f} micro-rad/s")
 82    print(f"  East:  {omega_ie[1] * 1e6:.3f} micro-rad/s")
 83    print(f"  Down:  {omega_ie[2] * 1e6:.3f} micro-rad/s")
 84    print(f"  Total: {np.linalg.norm(omega_ie) * 180 / np.pi * 3600:.4f} deg/hr")
 85
 86    # Radii of curvature
 87    R_n, R_e = radii_of_curvature(lat)
 88    print("\nRadii of curvature:")
 89    print(f"  Meridian (R_n): {R_n / 1e6:.3f} Mm")
 90    print(f"  Prime vertical (R_e): {R_e / 1e6:.3f} Mm")
 91
 92
 93def imu_processing_demo() -> None:
 94    """Demonstrate IMU data processing."""
 95    print("\n" + "=" * 60)
 96    print("2. IMU DATA PROCESSING")
 97    print("=" * 60)
 98
 99    np.random.seed(42)
100
101    # Simulated IMU data for a stationary sensor
102    dt = 0.01  # 100 Hz IMU
103
104    # Small biases and noise (typical MEMS IMU)
105    gyro_bias = np.array([0.001, -0.0005, 0.002])  # rad/s
106    accel_bias = np.array([0.01, -0.02, 0.015])  # m/s^2
107
108    # Generate IMU samples
109    n_samples = 100
110    print(f"\nSimulating {n_samples} IMU samples at {1 / dt:.0f} Hz")
111    print(
112        f"  Gyro bias: [{gyro_bias[0] * 1e3:.2f}, {gyro_bias[1] * 1e3:.2f}, {gyro_bias[2] * 1e3:.2f}] mrad/s"
113    )
114    print(
115        f"  Accel bias: [{accel_bias[0] * 1e3:.1f}, {accel_bias[1] * 1e3:.1f}, {accel_bias[2] * 1e3:.1f}] mm/s^2"
116    )
117
118    # Stationary sensor: gyro measures Earth rate, accel measures gravity
119    lat = np.radians(37.7749)
120    omega_ie = earth_rate_ned(lat)
121    g_ned = gravity_ned(lat, 0)
122
123    # Transform to body frame (assume level, north-pointing)
124    omega_body = omega_ie + gyro_bias + 1e-4 * np.random.randn(3)
125    accel_body = -g_ned + accel_bias + 0.01 * np.random.randn(3)
126
127    print("\nRaw IMU readings (stationary):")
128    print(
129        f"  Gyro: [{omega_body[0] * 1e3:.3f}, {omega_body[1] * 1e3:.3f}, {omega_body[2] * 1e3:.3f}] mrad/s"
130    )
131    print(
132        f"  Accel: [{accel_body[0]:.4f}, {accel_body[1]:.4f}, {accel_body[2]:.4f}] m/s^2"
133    )
134
135    # Coning correction (for high-frequency angular motion)
136    # Simulate angular increments
137    alpha_prev = omega_body * dt + 1e-6 * np.random.randn(3)
138    alpha_curr = omega_body * dt + 1e-6 * np.random.randn(3)
139
140    coning = coning_correction(alpha_prev, alpha_curr)
141    print(f"\nConing correction magnitude: {np.linalg.norm(coning) * 1e9:.3f} nano-rad")
142
143    # Sculling correction
144    dv_prev = accel_body * dt
145    dv_curr = accel_body * dt
146
147    sculling = sculling_correction(alpha_prev, alpha_curr, dv_prev, dv_curr)
148    print(
149        f"Sculling correction magnitude: {np.linalg.norm(sculling) * 1e9:.3f} nano-m/s"
150    )
151
152
153def coarse_alignment_demo() -> None:
154    """Demonstrate INS coarse alignment (leveling)."""
155    print("\n" + "=" * 60)
156    print("3. INS COARSE ALIGNMENT (LEVELING)")
157    print("=" * 60)
158
159    # Location
160    lat = np.radians(37.7749)
161    alt = 100.0
162
163    # Simulated accelerometer measurements (stationary)
164    g_ned = gravity_ned(lat, alt)
165
166    # True attitude: small roll and pitch
167    true_roll = np.radians(2.0)
168    true_pitch = np.radians(-1.5)
169
170    print("\nTrue attitude:")
171    print(f"  Roll: {np.degrees(true_roll):.2f} deg")
172    print(f"  Pitch: {np.degrees(true_pitch):.2f} deg")
173    print("  (Heading cannot be determined from accelerometers alone)")
174
175    # Construct rotation matrix (NED to body)
176    cr, sr = np.cos(true_roll), np.sin(true_roll)
177    cp, sp = np.cos(true_pitch), np.sin(true_pitch)
178
179    # Rotation matrices (heading assumed 0 for simplicity)
180    R_roll = np.array([[1, 0, 0], [0, cr, sr], [0, -sr, cr]])
181    R_pitch = np.array([[cp, 0, -sp], [0, 1, 0], [sp, 0, cp]])
182
183    C_bn = R_roll @ R_pitch  # Body from NED (no heading rotation)
184
185    # Body-frame measurements
186    # Accelerometer measures reaction to gravity (specific force)
187    accel_body = C_bn @ (-g_ned)
188
189    # Add small noise
190    np.random.seed(42)
191    accel_body += 0.005 * np.random.randn(3)
192
193    print("\nBody-frame accelerometer reading:")
194    print(
195        f"  Accel: [{accel_body[0]:.4f}, {accel_body[1]:.4f}, {accel_body[2]:.4f}] m/s^2"
196    )
197    print(f"  (For level vehicle: [0, 0, {-normal_gravity(lat, alt):.4f}] m/s^2)")
198
199    # Coarse alignment (leveling only - uses accelerometer to find roll/pitch)
200    roll_est, pitch_est = coarse_alignment(accel_body, lat)
201
202    print("\nEstimated attitude (coarse leveling):")
203    print(
204        f"  Roll: {np.degrees(roll_est):.2f} deg (error: {np.degrees(roll_est - true_roll):.3f} deg)"
205    )
206    print(
207        f"  Pitch: {np.degrees(pitch_est):.2f} deg (error: {np.degrees(pitch_est - true_pitch):.3f} deg)"
208    )
209
210    # Note about gyrocompassing
211    print("\nNote: Heading estimation requires gyrocompassing (sensing Earth")
212    print("rotation with gyroscopes), which is separate from coarse leveling.")
213
214
215def ins_mechanization_demo() -> None:
216    """Demonstrate INS mechanization."""
217    print("\n" + "=" * 60)
218    print("4. INS MECHANIZATION")
219    print("=" * 60)
220
221    # Initialize INS state
222    lat = np.radians(37.7749)
223    lon = np.radians(-122.4194)
224    alt = 100.0
225    vN, vE, vD = 10.0, 5.0, -1.0  # Moving NE, slight descent
226
227    state = initialize_ins_state(lat, lon, alt, vN=vN, vE=vE, vD=vD)
228
229    print("\nInitial state:")
230    print(
231        f"  Position: {np.degrees(state.latitude):.6f}N, {np.degrees(state.longitude):.6f}E, {state.altitude:.1f}m"
232    )
233    print(
234        f"  Velocity (NED): [{state.velocity[0]:.2f}, {state.velocity[1]:.2f}, {state.velocity[2]:.2f}] m/s"
235    )
236
237    # Simulate forward motion with IMU
238    dt = 0.01  # 100 Hz
239    n_steps = 100
240
241    # IMU measurements for constant velocity (no acceleration)
242    g_ned = gravity_ned(lat, alt)
243    omega_ie = earth_rate_ned(lat)
244    omega_en = transport_rate_ned(
245        state.velocity[0], state.velocity[1], state.latitude, state.altitude
246    )
247
248    # Body-frame measurements (assuming level flight, north heading)
249    accel_body = -g_ned  # Only gravity
250    gyro_body = omega_ie + omega_en  # Earth rate + transport rate
251
252    print(f"\nSimulating {n_steps} navigation steps at {1 / dt:.0f} Hz")
253
254    # Integrate
255    for _ in range(n_steps):
256        imu = IMUData(
257            gyro=gyro_body,
258            accel=accel_body,
259            dt=dt,
260        )
261        state = mechanize_ins_ned(state, imu)
262
263    print(f"\nFinal state after {n_steps * dt:.1f} seconds:")
264    print(
265        f"  Position: {np.degrees(state.latitude):.6f}N, {np.degrees(state.longitude):.6f}E, {state.altitude:.1f}m"
266    )
267    print(
268        f"  Velocity (NED): [{state.velocity[0]:.2f}, {state.velocity[1]:.2f}, {state.velocity[2]:.2f}] m/s"
269    )
270
271    # Expected position change
272    R_n, R_e = radii_of_curvature(lat)
273    _expected_dlat = vN * n_steps * dt / (R_n + alt)  # noqa: F841
274    _expected_dlon = vE * n_steps * dt / ((R_e + alt) * np.cos(lat))  # noqa: F841
275
276    print("\nPosition change:")
277    print(
278        f"  North: {np.degrees(state.latitude - lat) * 111000:.1f} m (expected: {vN * n_steps * dt:.1f} m)"
279    )
280    print(
281        f"  East: {np.degrees(state.longitude - lon) * 111000 * np.cos(lat):.1f} m "
282        f"(expected: {vE * n_steps * dt:.1f} m)"
283    )
284
285
286def gnss_geometry_demo() -> None:
287    """Demonstrate GNSS geometry and DOP."""
288    print("\n" + "=" * 60)
289    print("5. GNSS GEOMETRY AND DOP")
290    print("=" * 60)
291
292    # User position
293    user_lat = np.radians(37.7749)
294    user_lon = np.radians(-122.4194)
295    user_alt = 100.0
296    user_lla = np.array([user_lat, user_lon, user_alt])
297
298    user_ecef = np.array(geodetic_to_ecef(user_lat, user_lon, user_alt, WGS84))
299    print(f"\nUser position: {np.degrees(user_lat):.4f}N, {np.degrees(user_lon):.4f}E")
300
301    # Simulated satellite positions (GPS constellation subset)
302    sat_positions = [
303        (30, 45, 20200e3),  # Lat, lon, alt (deg, deg, m)
304        (45, -90, 20200e3),
305        (15, -150, 20200e3),
306        (60, 0, 20200e3),
307        (-30, 90, 20200e3),
308        (0, 180, 20200e3),
309    ]
310
311    print("\nSatellite visibility:")
312    print("-" * 50)
313
314    visible_sats = []
315    for i, (lat_deg, lon_deg, alt) in enumerate(sat_positions):
316        lat = np.radians(lat_deg)
317        lon = np.radians(lon_deg)
318        pos_ecef = np.array(geodetic_to_ecef(lat, lon, alt, WGS84))
319
320        # Compute elevation and azimuth
321        el, az = satellite_elevation_azimuth(user_lla, pos_ecef)
322
323        if el > 0:  # Above horizon
324            visible_sats.append(pos_ecef)
325            print(
326                f"  PRN {i + 1}: El={np.degrees(el):.1f} deg, Az={np.degrees(az):.1f} deg"
327            )
328        else:
329            print(f"  PRN {i + 1}: Below horizon (El={np.degrees(el):.1f} deg)")
330
331    # Compute DOP from geometry matrix
332    if len(visible_sats) >= 4:
333        # Build geometry matrix (line-of-sight unit vectors + clock column)
334        H = np.zeros((len(visible_sats), 4))
335        for i, sat_ecef in enumerate(visible_sats):
336            los = sat_ecef - user_ecef
337            range_val = np.linalg.norm(los)
338            H[i, :3] = -los / range_val  # Unit vector toward satellite
339            H[i, 3] = 1.0  # Clock column
340
341        GDOP, PDOP, HDOP, VDOP = compute_dop(H)
342        print(f"\nDilution of Precision (DOP) with {len(visible_sats)} satellites:")
343        print(f"  GDOP: {GDOP:.2f}")
344        print(f"  PDOP: {PDOP:.2f}")
345        print(f"  HDOP: {HDOP:.2f}")
346        print(f"  VDOP: {VDOP:.2f}")
347
348        # Interpret DOP
349        if PDOP < 2:
350            quality = "Excellent"
351        elif PDOP < 5:
352            quality = "Good"
353        elif PDOP < 10:
354            quality = "Moderate"
355        else:
356            quality = "Poor"
357        print(f"  Position accuracy: {quality}")
358    else:
359        print(f"\nInsufficient satellites ({len(visible_sats)}) for DOP computation")
360
361
362def loose_coupling_demo() -> None:
363    """Demonstrate loosely-coupled INS/GNSS integration."""
364    print("\n" + "=" * 60)
365    print("6. LOOSELY-COUPLED INS/GNSS INTEGRATION")
366    print("=" * 60)
367
368    np.random.seed(42)
369
370    # Initialize INS state
371    lat = np.radians(37.7749)
372    lon = np.radians(-122.4194)
373    alt = 100.0
374    vN, vE, vD = 10.0, 5.0, 0.0
375
376    ins_state = initialize_ins_state(lat, lon, alt, vN=vN, vE=vE, vD=vD)
377
378    # Initialize INS/GNSS integrated state
379    pos_std = 5.0  # meters
380    vel_std = 0.1  # m/s
381    att_std = np.radians(0.5)  # rad
382
383    state = initialize_ins_gnss(
384        ins_state, position_std=pos_std, velocity_std=vel_std, attitude_std=att_std
385    )
386
387    print("\nInitial state:")
388    print(
389        f"  Position: {np.degrees(state.ins_state.latitude):.6f}N, "
390        f"{np.degrees(state.ins_state.longitude):.6f}E"
391    )
392    print(f"  Position std: {np.sqrt(state.error_cov[0, 0]):.2f} m")
393    print(f"  Velocity std: {np.sqrt(state.error_cov[3, 3]):.3f} m/s")
394
395    # Simulate navigation with GNSS updates
396    dt_ins = 0.01  # INS rate
397    dt_gnss = 1.0  # GNSS rate
398    n_gnss_epochs = 5
399
400    print(f"\nSimulating {n_gnss_epochs} GNSS epochs ({n_gnss_epochs} seconds)")
401    print("-" * 60)
402
403    # IMU measurements (constant velocity)
404    g_ned = gravity_ned(lat, alt)
405    accel_body = -g_ned
406    gyro_body = earth_rate_ned(lat)
407
408    for epoch in range(n_gnss_epochs):
409        # INS propagation between GNSS updates
410        for _ in range(int(dt_gnss / dt_ins)):
411            imu = IMUData(
412                gyro=gyro_body + 1e-5 * np.random.randn(3),
413                accel=accel_body + 0.01 * np.random.randn(3),
414                dt=dt_ins,
415            )
416            state = loose_coupled_predict(state, imu)
417
418        # GNSS measurement (with noise)
419        gnss_pos = np.array(
420            [
421                state.ins_state.latitude + np.random.randn() * 2e-6,
422                state.ins_state.longitude + np.random.randn() * 2e-6,
423                state.ins_state.altitude + np.random.randn() * 5.0,
424            ]
425        )
426        gnss_vel = np.array(
427            [
428                state.ins_state.velocity[0] + np.random.randn() * 0.05,
429                state.ins_state.velocity[1] + np.random.randn() * 0.05,
430                state.ins_state.velocity[2] + np.random.randn() * 0.1,
431            ]
432        )
433
434        gnss_meas = GNSSMeasurement(
435            position=gnss_pos,
436            velocity=gnss_vel,
437            position_cov=np.diag([3.0**2, 3.0**2, 6.0**2]),
438            velocity_cov=np.diag([0.05**2, 0.05**2, 0.1**2]),
439            time=epoch * dt_gnss,
440        )
441
442        # GNSS update
443        result = loose_coupled_update(state, gnss_meas)
444        state = result.state
445
446        print(
447            f"  Epoch {epoch + 1}: Position std = {np.sqrt(state.error_cov[0, 0]):.2f} m, "
448            f"Velocity std = {np.sqrt(state.error_cov[3, 3]):.4f} m/s"
449        )
450
451    print("\nFinal uncertainties:")
452    print(f"  Position (N): {np.sqrt(state.error_cov[0, 0]):.2f} m")
453    print(f"  Position (E): {np.sqrt(state.error_cov[1, 1]):.2f} m")
454    print(f"  Position (D): {np.sqrt(state.error_cov[2, 2]):.2f} m")
455    print(f"  Velocity (N): {np.sqrt(state.error_cov[3, 3]):.4f} m/s")
456
457
458def gnss_outage_demo() -> None:
459    """Demonstrate GNSS outage detection."""
460    print("\n" + "=" * 60)
461    print("7. GNSS OUTAGE DETECTION")
462    print("=" * 60)
463
464    np.random.seed(42)
465
466    # GNSS outage detection uses chi-squared test on innovations
467    # To detect measurement faults (spoofing, multipath, etc.)
468    innovation_cov = np.diag([3.0**2, 3.0**2, 6.0**2])
469
470    print("\nGNSS measurement fault detection using chi-squared test")
471    print("  Innovation covariance: diag([9, 9, 36]) m^2")
472
473    # Chi-squared threshold for 3 DOF (position), 95% confidence
474    threshold_95 = 7.815  # chi2.ppf(0.95, 3)
475
476    # Test with normal innovations
477    print("\nNormal innovations (should pass):")
478    for i in range(3):
479        innovation = np.random.multivariate_normal(np.zeros(3), innovation_cov)
480        fault = gnss_outage_detection(
481            innovation, innovation_cov, threshold=threshold_95
482        )
483        nis = innovation @ np.linalg.solve(innovation_cov, innovation)
484        print(f"  Sample {i + 1}: NIS={nis:.2f}, Fault={fault}")
485
486    # Test with biased innovations (simulating fault)
487    print("\nBiased innovations (should detect fault):")
488    fault_bias = np.array([15.0, -10.0, 20.0])  # Large bias
489    for i in range(3):
490        innovation = fault_bias + np.random.multivariate_normal(
491            np.zeros(3), innovation_cov
492        )
493        fault = gnss_outage_detection(
494            innovation, innovation_cov, threshold=threshold_95
495        )
496        nis = innovation @ np.linalg.solve(innovation_cov, innovation)
497        print(f"  Sample {i + 1}: NIS={nis:.2f}, Fault={fault}")
498
499    print(
500        "\nNote: NIS (Normalized Innovation Squared) should follow chi-squared distribution"
501    )
502    print(
503        f"      with {len(innovation)} DOF. Threshold={threshold_95:.2f} (95% confidence)"
504    )
505
506
507def main() -> None:
508    """Run INS/GNSS navigation demonstrations."""
509    print("\nINS/GNSS Navigation Examples")
510    print("=" * 60)
511    print("Demonstrating pytcl navigation capabilities")
512
513    ins_basics_demo()
514    imu_processing_demo()
515    coarse_alignment_demo()
516    ins_mechanization_demo()
517    gnss_geometry_demo()
518    loose_coupling_demo()
519    gnss_outage_demo()
520
521    # Visualization
522    visualize_navigation_trajectory()
523
524    print("\n" + "=" * 60)
525    print("Done!")
526    print("=" * 60)
527
528
529def visualize_navigation_trajectory() -> None:
530    """Visualize INS navigation trajectory."""
531    print("\nGenerating navigation trajectory visualization...")
532
533    # Simulate a trajectory
534    np.random.seed(42)
535    n_steps = 200
536
537    # Create a circular trajectory
538    t = np.linspace(0, 2 * np.pi, n_steps)
539    x = 1000 * np.cos(t)
540    y = 1000 * np.sin(t)
541    z = 50 * np.sin(2 * t)
542
543    # Add noise
544    x_noisy = x + 5 * np.random.randn(n_steps)
545    y_noisy = y + 5 * np.random.randn(n_steps)
546    z_noisy = z + 2 * np.random.randn(n_steps)
547
548    # Create 3D trajectory plot
549    fig = go.Figure()
550
551    fig.add_trace(
552        go.Scatter3d(
553            x=x,
554            y=y,
555            z=z,
556            mode="lines",
557            line=dict(color="blue", width=3),
558            name="True Trajectory",
559        )
560    )
561
562    fig.add_trace(
563        go.Scatter3d(
564            x=x_noisy,
565            y=y_noisy,
566            z=z_noisy,
567            mode="markers",
568            marker=dict(size=4, color="red", opacity=0.5),
569            name="Measured Position",
570        )
571    )
572
573    fig.update_layout(
574        title="INS Navigation Trajectory: True vs Measured",
575        scene=dict(
576            xaxis_title="X (m)",
577            yaxis_title="Y (m)",
578            zaxis_title="Z (m)",
579        ),
580        height=600,
581        width=800,
582    )
583
584    if SHOW_PLOTS:
585        fig.show()
586    else:
587        OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
588        fig.write_html(
589            str(OUTPUT_DIR / "ins_gnss_navigation.html"),
590            include_plotlyjs="cdn",
591            div_id="ins_gnss_navigation",
592        )
593
594
595if __name__ == "__main__":
596    main()

Running the Example

python examples/ins_gnss_navigation.py

See Also