Advanced Geometric Features#

This example demonstrates advanced features of the pyinterp.geometry module, including coordinate transformations, line interpolation, dateline handling, spatial indexing, and performance comparisons.

Topics Covered:

Coordinate Transformations:

  • Coordinates: LLA to ECEF conversion

  • Transformations between different spheroids

Line Operations:

Dateline Handling:

  • Creating and working with geometries crossing the International Date Line

Spatial Indexing:

  • RTree: Efficient spatial queries

Geodesic Strategies:

Let’s start by importing the necessary libraries.

import timeit

import cartopy.crs
import cartopy.feature
import matplotlib.pyplot
import numpy
from pyinterp.geometry import geographic
from pyinterp.core import config

Coordinate System Transformations#

The Coordinates class converts between geodetic latitude, longitude, and altitude (LLA) and Earth-Centered, Earth-Fixed (ECEF) coordinates.

wgs84 = geographic.Spheroid()
coords_wgs84 = geographic.Coordinates(wgs84)

# Generate random test points
generator = numpy.random.Generator(numpy.random.PCG64(0))
lon_samples = generator.uniform(-180.0, 180.0, 100_000)
lat_samples = generator.uniform(-90.0, 90.0, 100_000)
alt_samples = generator.uniform(-10_000, 100_000, 100_000)

print("Coordinate Transformations:")
print(f"  Converting {len(lon_samples)} points to ECEF...")

# Convert to ECEF
x, y, z = coords_wgs84.lla_to_ecef(
    lon_samples, lat_samples, alt_samples, num_threads=0
)
print(f"  X range: [{x.min():.2f}, {x.max():.2f}] m")
print(f"  Y range: [{y.min():.2f}, {y.max():.2f}] m")
print(f"  Z range: [{z.min():.2f}, {z.max():.2f}] m")

# Convert back to LLA
lon_restored, lat_restored, alt_restored = coords_wgs84.ecef_to_lla(
    x, y, z, num_threads=0
)

# Check round-trip accuracy
lon_error = numpy.abs(lon_samples - lon_restored).max()
lat_error = numpy.abs(lat_samples - lat_restored).max()
alt_error = numpy.abs(alt_samples - alt_restored).max()

print("\nRound-trip errors:")
print(f"  Longitude: {lon_error:.2e} degrees")
print(f"  Latitude: {lat_error:.2e} degrees")
print(f"  Altitude: {alt_error:.2e} m")
Coordinate Transformations:
  Converting 100000 points to ECEF...
  X range: [-6473791.46, 6475418.84] m
  Y range: [-6476146.88, 6467975.51] m
  Z range: [-6456397.08, 6456273.14] m

Round-trip errors:
  Longitude: 2.84e-14 degrees
  Latitude: 2.84e-14 degrees
  Altitude: 4.07e-09 m

Transforming Between Different Spheroids#

You can transform coordinates between different reference ellipsoids.

grs80 = geographic.Spheroid(6378137.0, 1 / 298.257222101)
coords_grs80 = geographic.Coordinates(grs80)

# Transform from WGS84 to GRS80
lon_grs80, lat_grs80, alt_grs80 = coords_wgs84.transform(
    coords_grs80,
    lon_samples[:1000],
    lat_samples[:1000],
    alt_samples[:1000],
    num_threads=0,
)

# Compute differences
lon_diff = numpy.abs(lon_samples[:1000] - lon_grs80).max()
lat_diff = numpy.abs(lat_samples[:1000] - lat_grs80).max()
alt_diff = numpy.abs(alt_samples[:1000] - alt_grs80).max()

print("\nWGS84 to GRS80 transformation differences:")
print(f"  Longitude: {lon_diff:.2e} degrees")
print(f"  Latitude: {lat_diff:.2e} degrees")
print(f"  Altitude: {alt_diff:.2e} m")
WGS84 to GRS80 transformation differences:
  Longitude: 2.84e-14 degrees
  Latitude: 9.43e-10 degrees
  Altitude: 1.05e-04 m

Benchmark transformations

elapsed = timeit.timeit(
    lambda: coords_wgs84.transform(
        coords_grs80,
        lon_samples[:1000],
        lat_samples[:1000],
        alt_samples[:1000],
        num_threads=0,
    ),
    number=10,
)
print(f"\nTransformation time (1000 points, 10 runs): {elapsed:.6f} seconds")
print(f"Average time per run: {elapsed / 10:.6f} seconds")
Transformation time (1000 points, 10 runs): 0.002257 seconds
Average time per run: 0.000226 seconds

Line Interpolation: Getting Points at Specific Distances#

The line_interpolate() function returns a point at a specified distance along a line.

# Create a path from Paris to New York
paris = geographic.Point(2.3488, 48.8534)
new_york = geographic.Point(-73.9385, 40.6643)

path = geographic.LineString(
    numpy.array([paris.lon, new_york.lon], dtype=numpy.float64),
    numpy.array([paris.lat, new_york.lat], dtype=numpy.float64),
)

# Get the total distance
total_distance = geographic.algorithms.length(path, spheroid=wgs84)
print("\nPath from Paris to New York:")
print(f"  Total distance: {total_distance * 1e-3:.2f} km")

# Interpolate points at regular intervals
num_waypoints = 10
waypoints = []
for i in range(num_waypoints + 1):
    distance = (total_distance * i) / num_waypoints
    waypoint = geographic.algorithms.line_interpolate(
        path, distance, spheroid=wgs84
    )
    waypoints.append(waypoint)
    print(
        f"  Waypoint {i}: ({waypoint.lon:.4f}, {waypoint.lat:.4f}) "
        f"at {distance * 1e-3:.2f} km"
    )
Path from Paris to New York:
  Total distance: 5851.42 km
  Waypoint 0: (2.3488, 48.8534) at 0.00 km
  Waypoint 1: (-5.3328, 50.5528) at 585.14 km
  Waypoint 2: (-13.4918, 51.7114) at 1170.28 km
  Waypoint 3: (-21.9662, 52.2787) at 1755.42 km
  Waypoint 4: (-30.5393, 52.2276) at 2340.57 km
  Waypoint 5: (-38.9757, 51.5607) at 2925.71 km
  Waypoint 6: (-47.0656, 50.3093) at 3510.85 km
  Waypoint 7: (-54.6582, 48.5271) at 4095.99 km
  Waypoint 8: (-61.6727, 46.2801) at 4681.13 km
  Waypoint 9: (-68.0908, 43.6373) at 5266.27 km
  Waypoint 10: (-73.9384, 40.6642) at 5851.42 km

Curvilinear Distance: Distance Along a Path#

The curvilinear_distance() function computes the cumulative distance from the start of a line.

# Create a more complex path
lon_path = numpy.array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=numpy.float64)
lat_path = numpy.array([0.0, 1.0, 0.5, 1.5, 1.0], dtype=numpy.float64)
complex_path = geographic.LineString(lon_path, lat_path)

curv_distances = geographic.algorithms.curvilinear_distance(
    complex_path, spheroid=wgs84
)

print("\nCurvilinear distances along path:")
for i, (lon, lat, dist) in enumerate(
    zip(lon_path, lat_path, curv_distances, strict=False)
):
    print(
        f"  Point {i} ({lon:.2f}, {lat:.2f}): {dist * 1e-3:.3f} km from start"
    )
Curvilinear distances along path:
  Point 0 (0.00, 0.00): 0.000 km from start
  Point 1 (1.00, 1.00): 156.898 km from start
  Point 2 (2.00, 0.50): 281.181 km from start
  Point 3 (3.00, 1.50): 438.070 km from start
  Point 4 (4.00, 1.00): 562.338 km from start

Azimuth: Forward Bearing Between Points#

The azimuth() function computes the forward bearing from one point to another.

azimuth_pny = geographic.algorithms.azimuth(
    paris, new_york, spheroid=wgs84, strategy=geographic.algorithms.VINCENTY
)
azimuth_nyp = geographic.algorithms.azimuth(
    new_york, paris, spheroid=wgs84, strategy=geographic.algorithms.VINCENTY
)

print("\nAzimuth calculations:")
print(f"  Paris to New York: {numpy.degrees(azimuth_pny):.2f}°")
print(f"  New York to Paris: {numpy.degrees(azimuth_nyp):.2f}°")

# Note: Forward and backward azimuths are not simply 180° apart on a sphere
difference = abs(
    numpy.degrees(azimuth_pny) - (numpy.degrees(azimuth_nyp) - 180)
)
print(f"  Difference from 180°: {difference:.2f}°")
Azimuth calculations:
  Paris to New York: -68.26°
  New York to Paris: 53.72°
  Difference from 180°: 58.02°

Handling the International Date Line#

Geographic boxes and geometries that span the International Date Line (180°/-180° longitude) require special handling.

# Create a box that crosses the dateline
dateline_box = geographic.Box((170.0, -30.0), (-170.0, 30.0))

print("\nDateline-crossing box:")
print(
    f"  Min corner: ({dateline_box.min_corner.lon}, "
    f"{dateline_box.min_corner.lat})"
)
print(
    f"  Max corner: ({dateline_box.max_corner.lon}, "
    f"{dateline_box.max_corner.lat})"
)
Dateline-crossing box:
  Min corner: (170.0, -30.0)
  Max corner: (190.0, 30.0)

Test points around the dateline

test_points = [
    (170, 0, "Left edge (170°E)"),
    (175, 0, "Eastern section (175°E)"),
    (180, 10, "On dateline (180°)"),
    (-180, -10, "On dateline (-180°)"),
    (-175, 0, "Western section (-175°W)"),
    (-170, 0, "Right edge (-170°W)"),
    (0, 0, "Prime meridian (outside)"),
    (160, 0, "West of box (160°E)"),
    (-160, 0, "East of box (-160°W)"),
]

print("\nPoint-in-box tests:")
print(f"{'Longitude':>10} {'Latitude':>10} {'Within?':>10} {'Description'}")
print("-" * 70)

for lon, lat, description in test_points:
    point = geographic.Point(lon, lat)
    is_inside = geographic.algorithms.within(point, dateline_box)
    status = "Yes" if is_inside else "No"
    print(f"{lon:>10.1f} {lat:>10.1f} {status:>10}  {description}")
Point-in-box tests:
 Longitude   Latitude    Within? Description
----------------------------------------------------------------------
     170.0        0.0         No  Left edge (170°E)
     175.0        0.0        Yes  Eastern section (175°E)
     180.0       10.0        Yes  On dateline (180°)
    -180.0      -10.0        Yes  On dateline (-180°)
    -175.0        0.0        Yes  Western section (-175°W)
    -170.0        0.0         No  Right edge (-170°W)
       0.0        0.0         No  Prime meridian (outside)
     160.0        0.0         No  West of box (160°E)
    -160.0        0.0         No  East of box (-160°W)

RTree Spatial Indexing#

The RTree class provides efficient spatial indexing for fast nearest-neighbor queries.

# Create an RTree and populate it with random points
rtree = geographic.RTree()

# Generate random data
n_points = 10_000
data_lon = generator.uniform(-180.0, 180.0, n_points)
data_lat = generator.uniform(-60.0, 60.0, n_points)
data_values = generator.uniform(0.0, 100.0, n_points)

# Create coordinate array (lon, lat)
coordinates = numpy.column_stack([data_lon, data_lat])

# Insert into RTree
rtree.insert(coordinates, data_values)

print("\nRTree Spatial Index:")
print(f"  Indexed points: {rtree.size()}")
print(f"  Is empty: {rtree.empty()}")

# Get bounds
bounds = rtree.bounds()
if bounds:
    min_bounds, max_bounds = bounds
    print(
        f"  Bounds: ({min_bounds[0]:.2f}, {min_bounds[1]:.2f}) to "
        f"({max_bounds[0]:.2f}, {max_bounds[1]:.2f})"
    )
RTree Spatial Index:
  Indexed points: 10000
  Is empty: False
  Bounds: (-180.00, -60.00) to (180.00, 59.99)

Query nearest neighbors

query_points = numpy.array(
    [[0.0, 0.0], [45.0, 45.0], [-120.0, 30.0]], dtype=numpy.float64
)

settings = config.rtree.Query()

# Query the RTree
distances, values = rtree.query(query_points, settings)

print("\nNearest neighbor queries:")
for i, (qlon, qlat) in enumerate(query_points):
    print(f"  Query point ({qlon}, {qlat}):")
    for j, (dist, val) in enumerate(
        zip(distances[i], values[i], strict=False)
    ):
        if not numpy.isnan(dist):
            print(
                f"    Neighbor {j}: distance={dist * 1e-3:.2f} km, "
                f"value={val:.2f}"
            )
Nearest neighbor queries:
  Query point (0.0, 0.0):
    Neighbor 0: distance=98.45 km, value=86.00
    Neighbor 1: distance=107.51 km, value=27.11
    Neighbor 2: distance=175.07 km, value=25.24
    Neighbor 3: distance=209.22 km, value=7.36
    Neighbor 4: distance=256.15 km, value=79.94
    Neighbor 5: distance=266.47 km, value=35.39
    Neighbor 6: distance=272.78 km, value=97.55
    Neighbor 7: distance=281.96 km, value=42.04
  Query point (45.0, 45.0):
    Neighbor 0: distance=69.48 km, value=83.78
    Neighbor 1: distance=104.53 km, value=39.88
    Neighbor 2: distance=107.08 km, value=47.61
    Neighbor 3: distance=151.05 km, value=55.43
    Neighbor 4: distance=168.51 km, value=60.57
    Neighbor 5: distance=180.96 km, value=72.21
    Neighbor 6: distance=237.06 km, value=61.89
    Neighbor 7: distance=276.98 km, value=13.04
  Query point (-120.0, 30.0):
    Neighbor 0: distance=58.70 km, value=73.69
    Neighbor 1: distance=237.77 km, value=46.14
    Neighbor 2: distance=268.35 km, value=17.99
    Neighbor 3: distance=286.30 km, value=20.71
    Neighbor 4: distance=290.66 km, value=87.29
    Neighbor 5: distance=292.54 km, value=23.18
    Neighbor 6: distance=294.91 km, value=74.25
    Neighbor 7: distance=309.41 km, value=60.48

Geodesic Strategy Performance Comparison#

Different geodesic calculation strategies offer different trade-offs between accuracy and performance.

# Test points
london = geographic.Point(-0.1276, 51.5074)
tokyo = geographic.Point(139.6917, 35.6895)

strategies = [
    (geographic.algorithms.ANDOYER, "ANDOYER"),
    (geographic.algorithms.THOMAS, "THOMAS"),
    (geographic.algorithms.VINCENTY, "VINCENTY"),
    (geographic.algorithms.KARNEY, "KARNEY"),
]

print("\nGeodesic strategy comparison (London to Tokyo):")
print(f"{'Strategy':<15} {'Distance (km)':<15} {'Time (µs)':<15}")
print("-" * 50)

for strategy, name in strategies:
    # Calculate distance
    distance = geographic.algorithms.distance(
        london, tokyo, spheroid=wgs84, strategy=strategy
    )

    # Benchmark
    elapsed = timeit.timeit(
        lambda strategy=strategy: geographic.algorithms.distance(
            london, tokyo, spheroid=wgs84, strategy=strategy
        ),
        number=1000,
    )
    time_per_call = (elapsed / 1000) * 1e6  # Convert to microseconds

    print(f"{name:<15} {distance * 1e-3:<15.3f} {time_per_call:<15.3f}")
Geodesic strategy comparison (London to Tokyo):
Strategy        Distance (km)   Time (µs)
--------------------------------------------------
ANDOYER         9582.297        0.231
THOMAS          9582.302        0.286
VINCENTY        9582.302        0.551
KARNEY          19993.535       9.041

Azimuth calculation with different strategies

print("\nAzimuth calculation (London to Tokyo):")
print(f"{'Strategy':<15} {'Azimuth (degrees)':<20}")
print("-" * 40)

for strategy, name in strategies:
    azimuth = geographic.algorithms.azimuth(
        london, tokyo, spheroid=wgs84, strategy=strategy
    )
    print(f"{name:<15} {numpy.degrees(azimuth):<20.6f}")
Azimuth calculation (London to Tokyo):
Strategy        Azimuth (degrees)
----------------------------------------
ANDOYER         31.653401
THOMAS          31.653339
VINCENTY        31.653339
KARNEY          119.294357

Spheroid Properties#

Explore various properties of reference ellipsoids.

print("\nSpheroid properties comparison:")
print(f"{'Property':<35} {'WGS84':<20} {'GRS80':<20}")
print("-" * 75)

properties = [
    ("Semi-major axis (m)", lambda s: f"{s.semi_major_axis:,.2f}"),
    ("Flattening", lambda s: f"{s.flattening:.10f}"),
    ("Semi-minor axis (m)", lambda s: f"{s.semi_minor_axis():,.2f}"),
    ("Mean radius (m)", lambda s: f"{s.mean_radius():,.2f}"),
    ("Authalic radius (m)", lambda s: f"{s.authalic_radius():,.2f}"),
    ("Volumetric radius (m)", lambda s: f"{s.volumetric_radius():,.2f}"),
    (
        "Equatorial circumference (m)",
        lambda s: f"{s.equatorial_circumference():,.2f}",
    ),
    (
        "First eccentricity squared",
        lambda s: f"{s.first_eccentricity_squared():.10f}",
    ),
    (
        "Second eccentricity squared",
        lambda s: f"{s.second_eccentricity_squared():.10f}",
    ),
]

for prop_name, prop_func in properties:
    wgs84_val = prop_func(wgs84)
    grs80_val = prop_func(grs80)
    print(f"{prop_name:<35} {wgs84_val:<20} {grs80_val:<20}")
Spheroid properties comparison:
Property                            WGS84                GRS80
---------------------------------------------------------------------------
Semi-major axis (m)                 6,378,137.00         6,378,137.00
Flattening                          0.0033528107         0.0033528107
Semi-minor axis (m)                 6,356,752.31         6,356,752.31
Mean radius (m)                     6,371,008.77         6,371,008.77
Authalic radius (m)                 6,371,007.18         6,371,007.18
Volumetric radius (m)               6,371,000.79         6,371,000.79
Equatorial circumference (m)        40,075,016.69        40,075,016.69
First eccentricity squared          0.0066943800         0.0066943800
Second eccentricity squared         0.0067394967         0.0067394968

Visualization: Advanced Features#

fig = matplotlib.pyplot.figure(figsize=(16, 12))

# Plot 1: Line interpolation waypoints
ax1 = fig.add_subplot(2, 3, 1, projection=cartopy.crs.PlateCarree())
ax1.add_feature(cartopy.feature.LAND, alpha=0.3)
ax1.add_feature(cartopy.feature.OCEAN, alpha=0.3)
ax1.add_feature(cartopy.feature.COASTLINE)
ax1.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False)
ax1.set_extent([-100, 30, 30, 60])

# Plot the great circle path
ax1.plot(
    [paris.lon, new_york.lon],
    [paris.lat, new_york.lat],
    "b-",
    linewidth=2,
    label="Great Circle",
    transform=cartopy.crs.Geodetic(),
)

# Plot waypoints
for i, wp in enumerate(waypoints):
    ax1.plot(
        wp.lon,
        wp.lat,
        "ro",
        markersize=8,
        transform=cartopy.crs.PlateCarree(),
        zorder=5,
    )
    if i % 2 == 0:  # Label every other waypoint
        ax1.text(
            wp.lon,
            wp.lat + 2,
            f"{i}",
            transform=cartopy.crs.PlateCarree(),
            fontsize=8,
            ha="center",
        )

ax1.plot(
    paris.lon,
    paris.lat,
    "g^",
    markersize=12,
    label="Paris",
    transform=cartopy.crs.PlateCarree(),
    zorder=6,
)
ax1.plot(
    new_york.lon,
    new_york.lat,
    "rv",
    markersize=12,
    label="New York",
    transform=cartopy.crs.PlateCarree(),
    zorder=6,
)

ax1.legend(loc="lower left")
ax1.set_title("Line Interpolation: Paris to New York")

# Plot 2: Curvilinear distance
ax2 = fig.add_subplot(2, 3, 2)
ax2.plot(lon_path, lat_path, "bo-", linewidth=2, markersize=8)
for lon, lat, dist in zip(lon_path, lat_path, curv_distances, strict=False):
    ax2.text(
        lon + 0.02,
        lat + 0.02,
        f"{dist * 1e-3:.1f} km",
        fontsize=9,
    )
ax2.grid(True, alpha=0.3)
ax2.set_xlabel("Longitude")
ax2.set_ylabel("Latitude")
ax2.set_title("Curvilinear Distance Along Path")

# Plot 3: Dateline-crossing box
ax3 = fig.add_subplot(
    2, 3, 3, projection=cartopy.crs.PlateCarree(central_longitude=180)
)
ax3.add_feature(cartopy.feature.LAND)
ax3.add_feature(cartopy.feature.OCEAN)
ax3.add_feature(cartopy.feature.COASTLINE)
ax3.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False)
ax3.set_extent([140, -140, -40, 40])

# Plot the box boundaries (need to handle dateline wrapping)
box_lon = [170, 190, 190, 170, 170]  # 190 = -170 + 360
box_lat = [-30, -30, 30, 30, -30]
ax3.plot(
    box_lon,
    box_lat,
    color="red",
    linewidth=3,
    transform=cartopy.crs.Geodetic(),
    label="Dateline Box",
)

# Plot test points
for lon, lat, _ in test_points:
    point = geographic.Point(lon, lat)
    is_inside = geographic.algorithms.within(point, dateline_box)
    color = "green" if is_inside else "gray"
    marker = "o" if is_inside else "x"
    ax3.plot(
        lon,
        lat,
        marker,
        color=color,
        markersize=10,
        markeredgecolor="black",
        markeredgewidth=1,
        transform=cartopy.crs.PlateCarree(),
    )

ax3.legend()
ax3.set_title("Dateline-Crossing Box")

# Plot 4: RTree spatial index
ax4 = fig.add_subplot(2, 3, 4)
scatter = ax4.scatter(
    data_lon, data_lat, c=data_values, s=1, cmap="viridis", alpha=0.5
)
matplotlib.pyplot.colorbar(scatter, ax=ax4, label="Value")

# Plot query points and their nearest neighbors
for qlon, qlat in query_points:
    ax4.plot(
        qlon,
        qlat,
        "r*",
        markersize=15,
        markeredgecolor="black",
        markeredgewidth=1,
    )

ax4.grid(True, alpha=0.3)
ax4.set_xlabel("Longitude")
ax4.set_ylabel("Latitude")
ax4.set_title(f"RTree Spatial Index ({n_points:,} points)")
ax4.set_xlim(-180, 180)
ax4.set_ylim(-60, 60)

# Plot 5: Geodesic strategy comparison
ax5 = fig.add_subplot(2, 3, 5)
strategy_names = [name for _, name in strategies]
distances_km = [
    geographic.algorithms.distance(
        london, tokyo, spheroid=wgs84, strategy=strategy
    )
    * 1e-3
    for strategy, _ in strategies
]

bars = ax5.bar(
    strategy_names,
    distances_km,
    color=["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"],
)
ax5.set_ylabel("Distance (km)")
ax5.set_title("Geodesic Strategy Distance Comparison")
ax5.grid(True, alpha=0.3, axis="y")

# Add value labels on bars
for bar, dist in zip(bars, distances_km, strict=False):
    height = bar.get_height()
    ax5.text(
        bar.get_x() + bar.get_width() / 2.0,
        height,
        f"{dist:.1f}",
        ha="center",
        va="bottom",
        fontsize=9,
    )

# Plot 6: Coordinate transformation accuracy
ax6 = fig.add_subplot(2, 3, 6)

# Sample a subset for visualization
rng = numpy.random.default_rng(42)
sample_size = 1000
sample_indices = rng.choice(len(lon_samples), sample_size, replace=False)

lon_sample = lon_samples[sample_indices]
lat_sample = lat_samples[sample_indices]
alt_sample = alt_samples[sample_indices]
# Transform to ECEF and back
x_s, y_s, z_s = coords_wgs84.lla_to_ecef(
    lon_sample, lat_sample, alt_sample, num_threads=0
)
lon_s, lat_s, alt_s = coords_wgs84.ecef_to_lla(x_s, y_s, z_s, num_threads=0)

# Calculate errors
lon_err = numpy.abs(lon_sample - lon_s) * 1e6  # Convert to micro-degrees
lat_err = numpy.abs(lat_sample - lat_s) * 1e6
alt_err = numpy.abs(alt_sample - alt_s)  # Keep in meters

# Plot error distributions
ax6.hist(lon_err, bins=50, alpha=0.5, label="Longitude (µ°)", color="blue")
ax6.hist(lat_err, bins=50, alpha=0.5, label="Latitude (µ°)", color="red")
ax6.set_xlabel("Error (micro-degrees)")
ax6.set_ylabel("Frequency")
ax6.set_title("LLA ↔ ECEF Round-Trip Errors")
ax6.legend()
ax6.grid(True, alpha=0.3)
ax6.set_yscale("log")

matplotlib.pyplot.tight_layout()
Line Interpolation: Paris to New York, Curvilinear Distance Along Path, Dateline-Crossing Box, RTree Spatial Index (10,000 points), Geodesic Strategy Distance Comparison, LLA ↔ ECEF Round-Trip Errors

Total running time of the script: (0 minutes 1.691 seconds)

Gallery generated by Sphinx-Gallery