Skip to content

Conversation

BrettRD
Copy link
Contributor

@BrettRD BrettRD commented Aug 8, 2025

This PR follows a discussion on the mailing list about clock recovery for continuous-time trajectories.
I got the ok from my team to push the abstract features upstream, and I've re-written the implementation to be more versatile and line up better with existing literature.

I'm not ready to merge just yet (the header file preambles are conspicuously missing and the comments are still a bit of a mess), but I'd like some feedback for where the files should live, name-spacing, etc.
I'd also like some guidance on how best to expose Expression based features to the python bindings.

Basic usage:

  // instantiate the expression factory (uses cubic splines by default)
  TrajectoryModel<Pose3> model();

  // add some control points
  for(int i=0;i<10;i++){
    model.add_control_point(Pose3_(Key('p', i)));
  }

  size_t n; 
  std::vector<Pose3> measurements;

  // construct an Expression that samples the trajectory at the Nth timestamp
  Pose3_ sample = model.sample_trajectory(Double_(Key('t', n)));

  // construct the factor
  f = ExpressionFactor(
    /*noise model*/
    measurement[n],
    sample,
  );
 // add f to the factor graph

Features:

  • Differentiable cardinal splines up to 7th order (higher orders are easy to add).
  • Nth derivatives of splines exposed as Expressions (necessarily including Jacobians)
  • spline abstraction also covers FIR filters
  • computational complexity can use lower bound of O(spline_order+1)
  • unknown timestamps represented with computational complexity of O(spline_order + timestamp_search_window)
  • Template type for CartesianProducts of existing manifolds (like a std::pair but inheriting from LieGroup).
  • Template type AsVectorSpace for extrapolating VectorSpace features from Manifold traits
  • N-dimensional cardinal spline meshes (surface patches are a happy accidental feature)

Miscellany:

  • Expression template for expmap (probably needs a quick test)

unused features that can be be removed from the PR:

  • Irwin Hall Probability Density Function (only the CDF is required)
  • Expression templates extended for operators on Double_ types

Implementation

Spline implementation follows a CVPR paper where the weights are pre-computed, and the weighted sum happens in the Lie Algebra using the first point (of the points in the window) as the datum.
This results in an execution that can act on a minimal number of control points, considers each point once, and has a trivial derivative propagation rule.
The weights for (uniformly sampled) splines are captured by the Irwin Hall Cumulative Probability Density Function.
The (uniformly sampled) weighted sum happens to be structurally equivalent to a Finite Impulse Response (FIR) Filter.

Questions:

  • Do the navigation and IMU features set down a type convention for angular velocity? and does it differ from traits<Rot3>::TangentVector?
  • traits<T>::TangentVector is a sensible type for first derivatives of T, but is it an adequate type for second and Nth derivatives?
  • The spline curve system is implemented under a type called TrajectoryModel, but it's capable of defining surface-patches. what's a better name for it?
  • What wrapping API would be required to use this from Python?
    • add keys to serve as control points?
    • generate factors?

Missing Tests:

  • cross-check against the fixed-order implementation by @koide3
  • Analytic derivatives of Rot3 spline curves match numerical derivatives to Nth order
  • Let me know if there's anything that you notice

Missing Examples:

  • Factor graph associating raw IMU data directly to derivatives of the trajectory in order to recover the time offset between non-monotonic, asynchronous, Pose3 and IMU data-sources Furgale 2013

Missing Features:

  • Cal3 doesn't have an expmap, so powered zoom lenses can't be built into a coordinated motion model using TrajectoryModel<CartesianProduct<Pose3, Cal3> > A Riemannian embedding using the localCoordinates call would be (barely) enough like a logmap for this spline to function. See AsVectorSpace

@ProfFan ProfFan requested a review from dellaert August 9, 2025 06:35
@dellaert
Copy link
Member

dellaert commented Aug 9, 2025

@BrettRD this is amazing!

Here are my overall (Codex-assisted!) comments without yet trying to understand the code in detail. The first two comments are obvious/easy as they relate to style/comment currently requested for GTSAM PRs. The third one is way less obvious and might need a face-to-face meeting so I can help think through it with you:

Code style

  • the files can go straight in the basis folder, not a big fan of further cascading the folder structure
  • Please use PascalCase for class names and camelCase for methods and member variables to match the rest of the library. It’s basically Google but no snake_case
  • Ensure all files pass clang-format with the repository’s configuration .clang-format (based on Google style). Running clang-format -i on changed files before committing will keep the style consistent.

Documentation

Please add Doxygen comments to new public classes, methods, and major typedefs in header files. Briefly describe the intent, parameters, and return values; this improves discoverability and helps future contributors.

The Basis framework

Primer on the gtsam/basis framework

Basis.h defines a CRTP base class Basis that supplies utility functors:

  • CalculateWeights / DerivativeWeights (static in each derived class) compute the basis-function values and derivatives for a given order N at point x.
  • EvaluationFunctor and VectorEvaluationFunctor evaluate a basis at runtime and return optional Jacobians.
  • WeightMatrix builds matrices of basis weights for multiple evaluation points.
    Specific bases (e.g., Chebyshev1Basis, Chebyshev2Basis, FourierBasis) implement these static methods and typedef Parameters to encode their coefficient structure.
    BasisFactors.h wraps these functors into factor graph factors (EvaluationFactor, VectorEvaluationFactor, etc.) that enforce measurements on scalar or vector-valued parameterizations.

Integrating your spline work

  • If the spline uses a new polynomial or kernel basis, create a MySplineBasis class inheriting from Basis and implement CalculateWeights and DerivativeWeights.
  • Expose evaluation through the provided functors so you can reuse BasisFactors or FitBasis for optimization and interpolation.
  • Reuse existing naming and spacing conventions (e.g., MySplineBasis::Parameters) and keep the interface consistent with current basis classes—this will allow existing tooling (factors, tests, potential Python bindings) to work with minimal adjustments.

That being said, we should have a chat about what that might look like here, and whether it fits your code.

@dellaert
Copy link
Member

dellaert commented Aug 9, 2025

PS calibrations objects satisfy the manifold traits, but not (yet) Lie group. It does not make obvious sense to “compose” 2 calibration objects, and reading the CVPR paper the cumulative spline representation seems to demand composition and inverse.

You could, in an application, “hack” this by deriving from a Calibration type, adding the composition / addition, and make that a Lie group / vector space, with corresponding traits.

@BrettRD
Copy link
Contributor Author

BrettRD commented Aug 11, 2025

I'll have another read of the basis framework. On a first read, it looked like a lot of machinery to handle butterfly calculations in bounded domains and it wasn't clear if any of it could be adapted to single evaluations of FIR taps.

For Cal3, I suspect every choice of manifold embedding is going to be equally arbitrary, so I'm tempted to make a simple CRTP adaptor to hack Vector-space traits onto the side of Manifold, based on a naive Reimannian embedding.
That way, Manifold classes can remain only Manifold, and users who to need to pretend it's a more sophisticated manifold can apply a wrapping of Reimannain<Cal3>, making the hackyness an explicit user choice, while still being a well-behaved and easy-to-use type.

@dellaert
Copy link
Member

I'll have another read of the basis framework. On a first read, it looked like a lot of machinery to handle butterfly calculations in bounded domains and it wasn't clear if any of it could be adapted to single evaluations of FIR taps.

Indeed, Basis is designed to create functors (in BasisFactors.h) at single "taps" in bounded domains. But, if you have a trajectory with $t\in[a,b]$, you do have a bounded domain. I think the issue is that CalculateWeights should yield a sparse matrix in the case of splines.

For Cal3, I suspect every choice of manifold embedding is going to be equally arbitrary, so I'm tempted to make a simple CRTP adaptor to hack Vector-space traits onto the side of Manifold, based on a naive Riemannian embedding. That way, Manifold classes can remain only Manifold, and users who to need to pretend it's a more sophisticated manifold can apply a wrapping of Riemannian<Cal3>, making the hackyness an explicit user choice, while still being a well-behaved and easy-to-use type.

I like it. Maybe AsVectorSpace ? It definitely "works" - I think we had addition at one point - but it felt wrong. They are not even groups. For the simplest $K$ calibration, they are all affine transforms, but already $K^{-1}$ is not a valid calibration.

@BrettRD
Copy link
Contributor Author

BrettRD commented Aug 13, 2025

I like it. Maybe AsVectorSpace ? It definitely "works" - I think we had addition at one point - but it felt wrong. They are not even groups. For the simplest K calibration, they are all affine transforms, but already K − 1 is not a valid calibration.

Something like that. I really ought to split this PR

The third one is way less obvious and might need a face-to-face meeting so I can help think through it with you

I sent an email to your gatech address, is that the best way to reach you?

@dellaert dellaert requested a review from Copilot August 21, 2025 11:57
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a Lie-Group aware differentiable spline curve implementation to enable clock recovery for continuous-time trajectories. The implementation follows CVPR 2020 research on efficient derivative computation for cumulative B-splines on Lie Groups.

Key changes:

  • Adds TrajectoryModel template class for cardinal spline interpolation up to 7th order with Expression-based API
  • Implements CartesianProduct and AsVectorSpace template utilities for combining manifolds
  • Provides Irwin Hall polynomial kernels for spline basis functions
  • Extends Expression templates with expmap function and scalar multiplication operators

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
gtsam/slam/expressions.h Adds expmap Expression template and scalar multiplication operators
gtsam/geometry/CartesianProduct.h Template for combining unrelated manifolds into single data structure
gtsam/geometry/AsVectorSpace.h Adapter to add VectorSpace traits to Manifold-only classes
gtsam/basis/polynomial/TrajectoryModel.h Main spline interpolation Expression factory for continuous trajectories
gtsam/basis/polynomial/PiecewisePolynomial.h Base class for piecewise polynomial kernel functions
gtsam/basis/polynomial/IrwinHall.h Header declaring Irwin Hall polynomial coefficients
gtsam/basis/polynomial/IrwinHall.cpp Irwin Hall PDF coefficient implementations
gtsam/basis/polynomial/IrwinHallCDF.cpp Irwin Hall CDF coefficient implementations
gtsam/basis/polynomial/kernels.h Abstract base class for convolutional kernel functions
examples/ClockRecoveryContinuousTrajectory.cpp Example demonstrating clock recovery with asynchronous sensors
Test files Comprehensive unit tests for all new functionality
Comments suppressed due to low confidence (2)

gtsam/basis/polynomial/TrajectoryModel.h:134

  • Accessing 'kernel' as a public member when it's declared as 'const KernelBase& kernel_;' - should use 'trajectory_model.kernel_' instead.
   * 

gtsam/basis/polynomial/TrajectoryModel.h:143

  • Accessing 'kernel' as a public member when it should be 'accel_clock_model.kernel_' based on the class definition.
    Double_ timestamp,

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@@ -0,0 +1,148 @@
/**
* @file testCartesinProduct.cpp
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filename in comment has a typo: 'testCartesinProduct.cpp' should be 'testCartesianProduct.cpp'

Suggested change
* @file testCartesinProduct.cpp
* @file testCartesianProduct.cpp

Copilot uses AI. Check for mistakes.

@@ -0,0 +1,157 @@
/**
* @file testCartesinProduct.cpp
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong filename in comment: should be 'testAsVectorSpace.cpp' not 'testCartesinProduct.cpp'

Suggested change
* @file testCartesinProduct.cpp
* @file testAsVectorSpace.cpp

Copilot uses AI. Check for mistakes.

{
Key accel_clock_drift_key = Key('a', i);
Key gyro_clock_drift_key = Key('g', i);
initial_values.insert<double>(accel_drift_key, 0.0);
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable 'accel_drift_key' is undefined. Should be 'accel_clock_drift_key' based on the declaration on line 144.

Suggested change
initial_values.insert<double>(accel_drift_key, 0.0);
initial_values.insert<double>(accel_clock_drift_key, 0.0);

Copilot uses AI. Check for mistakes.



// sample each sensor
shutter_event_t shutter_event = sample_camera_add_slam_factors();
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function 'sample_camera_add_slam_factors()' is undefined and not declared anywhere in the file.

Copilot uses AI. Check for mistakes.


// sample each sensor
shutter_event_t shutter_event = sample_camera_add_slam_factors();
acceleration_t acceleration = sample_accelerometer();
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function 'sample_accelerometer()' is undefined and not declared anywhere in the file.

Copilot uses AI. Check for mistakes.

// sample each sensor
shutter_event_t shutter_event = sample_camera_add_slam_factors();
acceleration_t acceleration = sample_accelerometer();
angular_rate_t angular_rate = sample_gyro();
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function 'sample_gyro()' is undefined and not declared anywhere in the file.

Copilot uses AI. Check for mistakes.

for(int i=0; i<trajectory_model.getControlPoints().size(); i++){
auto smoothness_factor = ExpressionFactor<Vector6>(
/* any robust noise model */
Vector6()::Zero(),
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: 'Vector6()::Zero()' should be 'Vector6::Zero()' - cannot call static member function on temporary object.

Suggested change
Vector6()::Zero(),
Vector6::Zero(),

Copilot uses AI. Check for mistakes.

if (H) *H << I_3x3, Z_3x3;
return pose_derivative.seq(0,2);
}
Point3Deriv_ pose3DerivativeTranslation(Pose3Deriv_ pose_derivative, OptionalJacobian<3, 6> H = {}){
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function parameter should be passed by const reference for efficiency: 'const Pose3Deriv_& pose_derivative'

Suggested change
Point3Deriv_ pose3DerivativeTranslation(Pose3Deriv_ pose_derivative, OptionalJacobian<3, 6> H = {}){
Rot3Deriv_ pose3DerivativeRotation(const Pose3Deriv_& pose_derivative, OptionalJacobian<3, 6> H = {}){
if (H) *H << I_3x3, Z_3x3;
return pose_derivative.seq(0,2);
}
Point3Deriv_ pose3DerivativeTranslation(const Pose3Deriv_& pose_derivative, OptionalJacobian<3, 6> H = {}){

Copilot uses AI. Check for mistakes.

if (H) *H << I_3x3, Z_3x3;
return pose_derivative.seq(0,2);
}
Point3Deriv_ pose3DerivativeTranslation(Pose3Deriv_ pose_derivative, OptionalJacobian<3, 6> H = {}){
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function parameter should be passed by const reference for efficiency: 'const Pose3Deriv_& pose_derivative'

Suggested change
Point3Deriv_ pose3DerivativeTranslation(Pose3Deriv_ pose_derivative, OptionalJacobian<3, 6> H = {}){
Point3Deriv_ pose3DerivativeTranslation(const Pose3Deriv_& pose_derivative, OptionalJacobian<3, 6> H = {}){

Copilot uses AI. Check for mistakes.

)
{
if(Hk) *Hk << x;
if(Hx) *Hx << k * MakeJacobian<T,T>::type::Identity();
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: 'MakeJacobian<T,T>::type::Identity()' should likely be 'typename MakeJacobian<T,T>::type::Identity()' or use a different approach to get the identity matrix.

Suggested change
if(Hx) *Hx << k * MakeJacobian<T,T>::type::Identity();
if(Hx) *Hx << k * typename MakeJacobian<T,T>::type::Identity();

Copilot uses AI. Check for mistakes.

@dellaert
Copy link
Member

I liked many of Copilot's suggestions. Two more comments (and I might have more):

  • changes in gtsam/slam/expressions.h should probably move to gtsam/nonlinear/expressions.h
  • still many snake_case variables rather than camelCase

@BrettRD
Copy link
Contributor Author

BrettRD commented Aug 22, 2025

The majority of that copilot output seems to be about the example (which is only illustrative at this point and certainly won't compile)
The only remaining snake case outside of the example are stray local variables in tests, I'm holding off on any more style polishing until we can discuss some proposed changes to the Basis.h API so I can refactor everything in one go.

My last email might also be in your spam folder

@dellaert
Copy link
Member

FYI: saw this paper advertised on Linkedin https://arxiv.org/abs/2504.11580

@BrettRD
Copy link
Contributor Author

BrettRD commented Sep 13, 2025

FYI: saw this paper advertised on Linkedin https://arxiv.org/abs/2504.11580

Wonderful!
It's good to see the method being used in a Kalman filter, I've only used it in one-shot factor-graphs.

Their cubic basis function in (3) exactly matches the IrwinHall3 struct with a time-shift for each sample.
https://www.desmos.com/calculator/ucaaqvbevx
Calling out to a known distribution allows us to generalise the method over higher (and lower) order splines

Their approach to differentiate the polynomial that populates the time vector (7) is the same as in PiecewisePolynomial.h, giving it the connection to pseudo-spectral methods (pay quantisation errors up front, in exchange for exact derivatives thereafter)

I'm surprised to see them running 100 control points per second, that seems a bit much, though I haven't seen a (correct) analysis of how the bandwidth of the curve scales with control-point frequency and spline order. The piecewise approximation of a Gaussian is a fairly aggressive low-pass filter, and insufficient control point density can make the SO3 component wrap when trying to describe high angular accelerations.
I'd love to see an analysis that rigorously connects this method to Grace Wahba's smoothing splines, including how gtsam factors can be used to describe smoothness priors.

I haven't forgotten about the changes we discussed, I haven't had a suitable block of time to get familiar with Eigen sparse matrices and plan out how to concatenate block-wise parameters into a contiguous model

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants