Interpolation methods¶
SP3 orbits come every 5 or 15 minutes and CLK clocks every 30 s or 5
minutes; observations come every 1 to 30 s. canvod-auxiliary interpolates
one day of orbits and clocks onto a regular grid that starts at 00:00 and
has the sampling interval of the observations
(canvod.auxiliary.interpolation.interpolate_aux_day). Each observation
epoch then takes the nearest grid epoch. canvodpy run, the ephemeris
provider of the Python API and the Airflow tasks all use these functions.
-
Cubic Hermite spline
Used for SP3 orbits (satellite X, Y, Z).
The spline matches the positions and the satellite velocities at every SP3 epoch.
-
Piecewise linear
Used for CLK clock offsets.
Linear between neighboring clock epochs, never across a detected clock jump.
Cubic Hermite spline (orbits)¶
Orbital motion is smooth, so a cubic polynomial between two SP3 epochs
fits it well. A Hermite spline fixes each polynomial by the positions and
velocities at both ends of the interval
(scipy.interpolate.CubicHermiteSpline).
SP3 files read by canVODpy provide positions only. The SP3 reader
(Sp3File, add_velocities=True by default) estimates the velocities
from the positions by central differences (forward and backward
differences at the first and last epoch); a run computes them on the day
joined with its neighbors (see below), so the day's own first and last
epochs get central differences too.
from canvod.auxiliary.interpolation import Sp3Config, Sp3InterpolationStrategy
config = Sp3Config(
use_velocities=True, # Hermite spline with Vx, Vy, Vz
fallback_method="linear", # used when the dataset has no velocities
)
interpolator = Sp3InterpolationStrategy(config=config)
result = interpolator.interpolate(sp3_data, target_epochs)
| Parameter | Default | Effect |
|---|---|---|
use_velocities |
True |
Hermite spline if Vx, Vy, Vz are in the dataset |
fallback_method |
"linear" |
xarray interpolation method without velocities |
Start and end of the day¶
A daily orbit file can end before midnight (ESA's 15-minute rapid orbits
end at 23:45; CODE's 5-minute orbits include 24:00), and the velocities of
its first and last epoch are one-sided differences. So every run loads the
orbit and clock files of the previous and the next day too
(AuxDataPipeline.create_standard), keeps one hour of each
(AuxFile.join_days; where files overlap, the day's own epoch is kept) and
computes the velocities on the joined data. Every epoch of the day then
lies between two orbit epochs: nothing is extrapolated. If a neighboring
day's file cannot be obtained, the run stops with an error naming it.
Final products come out per GPS week, so the last day of a week can be
processed only once the next week's products are published.
The spline is fitted per satellite on the epochs with data. A target epoch outside a satellite's data, or in a gap of more than 1.5 sampling intervals, is NaN.
Accuracy¶
Measured on the CODE final orbits of 2025-001 (with 2024-366 and 2025-002), by leaving orbit epochs out and interpolating them from the rest; the angle is the largest direction error seen from the ground:
| Orbit sampling | Largest position error | Largest angle error |
|---|---|---|
| 10 min (positions at the 5-min epochs in between) | 81 m | \(0.0002^\circ\) |
| 15 min | 1.1 km | \(0.004^\circ\) |
With the day's file alone (before the neighbors were loaded), the first and last interval of the day were off by up to 37 km (\(0.11^\circ\)), and a 15-minute file ending at 23:45 was extrapolated by up to 990 km (\(2.9^\circ\)).
Piecewise linear (clocks)¶
Satellite clock offsets can jump. Linear interpolation between neighboring CLK epochs, split at each jump, avoids fitting across one.
from canvod.auxiliary.interpolation import ClockConfig, ClockInterpolationStrategy
config = ClockConfig(
window_size=9, # epochs averaged on each side of a candidate jump
jump_threshold=1e-6, # jump detection threshold (seconds)
)
interpolator = ClockInterpolationStrategy(config=config)
result = interpolator.interpolate(clk_data, target_epochs)
Jump detection
For each gap between consecutive CLK epochs, the interpolator compares
the mean of up to window_size epochs before the gap with the mean of
up to window_size epochs after it. Where the difference exceeds
jump_threshold, the gap is a jump (for a run of flagged gaps, only the
one with the largest difference). The series is split at each jump and
interpolated linearly within each segment, never across a jump. Target
epochs inside a jump gap, outside the data, or in a segment of a single
epoch are NaN. window_size=1 compares the two neighboring epochs only.
The strategy interpolates every data variable whose name contains
clock, clk, Clock or CLK, and raises ValueError if there is none.
Recorded in the data¶
interpolate_aux_day writes the class and settings of each strategy to
the interpolated orbits and clocks (attrs["interpolator_config"], from
Interpolator.to_attrs()), and
create_interpolator_from_attrs() rebuilds the interpolator from these
attributes.
Custom strategies¶
Subclass Interpolator to write your own interpolation:
import numpy as np
import xarray as xr
from canvod.auxiliary.interpolation import Interpolator
class CustomStrategy(Interpolator):
"""Example: Lagrange polynomial interpolation."""
def interpolate(
self,
ds: xr.Dataset,
target_epochs: np.ndarray,
) -> xr.Dataset:
# Your implementation here
return interpolated_ds
canvodpy run always uses Sp3InterpolationStrategy and
ClockInterpolationStrategy; a custom strategy is for your own scripts.