This notebook demonstrates how to fit a set of scalar measurements using
FitBasis for all four basis options exposed in the Python wrapper:
FourierBasisChebyshev1BasisChebyshev2BasisChebyshev2(pseudo-spectral, parameters are values at Chebyshev points)
We use a time domain from t = 0 to 1 and fit each basis with fewer
parameters than points. For Chebyshev bases, we map time into the
Chebyshev interval x = 2t - 1 to match the default [-1, 1] domain.
For the Fourier basis we use x = 2pi t to model periodic behavior.
import numpy as np
import gtsam
import plotly.graph_objects as go
np.set_printoptions(precision=3, suppress=True)
# Sample data
rng = np.random.default_rng(42)
num_points = 30
t = np.linspace(0.0, 1.0, num_points)
# A smooth, mildly periodic signal with a trend
y_clean = 0.7 * np.sin(2 * np.pi * t) + 0.3 * np.cos(4 * np.pi * t) + 0.2 * t
y = y_clean + 0.05 * rng.standard_normal(size=t.size)
# Parameter count must be smaller or equal than number of points
N = 10
def plot_fit(t_samples, y_samples, t_dense, y_fit, title, extra=None):
fig = go.Figure()
fig.add_trace(go.Scatter(x=t_samples, y=y_samples, mode="markers", name="Samples", marker=dict(color="blue", symbol="diamond")))
fig.add_trace(go.Scatter(x=t_dense, y=y_fit, mode="lines", name="Fit"))
if extra is not None:
fig.add_trace(extra)
fig.update_layout(
title=title,
xaxis_title="time t",
yaxis_title="value",
template="plotly_white",
width=900,
height=450,
)
fig.show()
FourierBasis Fit¶
This cell:
Builds a
{x: y}sample map using the periodic domainx = 2pi t.Fits Fourier coefficients with
FitBasisFourierBasis.Evaluates the fit on a dense grid via
FourierBasis.WeightMatrix.
Copy the core fit + evaluation parts if you want to integrate this into your own pipeline (plotting is in a helper function).
x_fourier = 2.0 * np.pi * t
sequence : dict = {float(xi): float(yi) for xi, yi in zip(x_fourier, y)}
model = gtsam.noiseModel.Isotropic.Sigma(1, 0.05)
fit = gtsam.FitBasisFourierBasis(sequence, model, N)
params = fit.parameters()
# Extend the time domain to show periodic behavior
t_dense = np.linspace(-0.5, 1.5, 600)
x_dense = 2.0 * np.pi * t_dense
W = gtsam.FourierBasis.WeightMatrix(len(params), x_dense)
y_fit = W @ params
plot_fit(t, y, t_dense, y_fit, "FourierBasis Fit (periodic beyond [0, 1])")
Chebyshev1Basis Fit¶
This cell:
Maps time to the Chebyshev interval with
x = 2t - 1.Fits Chebyshev-1 coefficients with
FitBasisChebyshev1Basis.Evaluates the fit using
Chebyshev1Basis.WeightMatrix.
Copy the fit + evaluation steps to reuse with your own data or noise model.
x_cheb = 2.0 * t - 1.0
sequence = {float(xi): float(yi) for xi, yi in zip(x_cheb, y)}
model = gtsam.noiseModel.Isotropic.Sigma(1, 0.05)
fit = gtsam.FitBasisChebyshev1Basis(sequence, model, N)
params = fit.parameters()
t_dense = np.linspace(-0.1, 1.1, 600)
x_dense = 2.0 * t_dense - 1.0
W = gtsam.Chebyshev1Basis.WeightMatrix(len(params), x_dense)
y_fit = W @ params
plot_fit(t, y, t_dense, y_fit, "Chebyshev1Basis Fit (extrapolation outside [0, 1])")
Chebyshev2Basis Fit¶
This cell uses the second-kind Chebyshev basis (coefficient form):
Map time to
x = 2t - 1.Fit with
FitBasisChebyshev2Basis.Evaluate with
Chebyshev2Basis.WeightMatrix.
These steps are the minimal pieces you need for non-plotting usage.
sequence = {float(xi): float(yi) for xi, yi in zip(x_cheb, y)}
model = gtsam.noiseModel.Isotropic.Sigma(1, 0.05)
fit = gtsam.FitBasisChebyshev2Basis(sequence, model, N)
params = fit.parameters()
t_dense = np.linspace(-0.1, 1.1, 600)
x_dense = 2.0 * t_dense - 1.0
W = gtsam.Chebyshev2Basis.WeightMatrix(len(params), x_dense)
y_fit = W @ params
plot_fit(t, y, t_dense, y_fit, "Chebyshev2Basis Fit (extrapolation outside [0, 1])")
Chebyshev2 (Pseudo-Spectral) Fit¶
This variant treats the parameters as function values at Chebyshev points. The steps here are slightly different conceptually:
Fit values at Chebyshev points with
FitBasisChebyshev2.Evaluate by barycentric interpolation using
Chebyshev2.WeightMatrix.Plot the interpolation nodes alongside the fitted curve.
If you don’t need plots, copy the fit + evaluation and skip the marker trace.
sequence = {float(xi): float(yi) for xi, yi in zip(x_cheb, y)}
model = gtsam.noiseModel.Isotropic.Sigma(1, 0.05)
fit = gtsam.FitBasisChebyshev2(sequence, model, N)
params = fit.parameters()
t_dense = np.linspace(-0.1, 1.1, 600)
x_dense = 2.0 * t_dense - 1.0
W = gtsam.Chebyshev2.WeightMatrix(len(params), x_dense)
y_fit = W @ params
cheb_points = gtsam.Chebyshev2.Points(N)
t_cheb = 0.5 * (cheb_points + 1.0)
y_cheb = params
markers = go.Scatter(x=t_cheb, y=y_cheb, mode="markers", name="Chebyshev2 points", marker=dict(color="red"))
plot_fit(t, y, t_dense, y_fit, "Chebyshev2 Pseudo-Spectral Fit (extrapolation outside [0, 1])", extra=markers)
The Chebyshev points above are the parameterization. They values at those points are moved up and down to make the blue points fit the polynomial as closely as possible. Because for every points we can exactly fit an degree polynomial, the values at these Chebyshev points are a parameterization of the polynomial.
Chebyshev points are fixed nodes that cluster near the interval ends; this improves interpolation stability and reduces endpoint oscillations compared to equally spaced nodes (mitigating the Runge phenomenon).