Operations on trajectories
Main author: Simon Rohou
Arithmetic and calculus
Operations such as x1+x2 or sin(x1) compute values at the stored timestamps. The resulting SampledTraj interpolates linearly between these values. Arithmetic between two sampled trajectories requires identical timestamps.
For a scalar or vector trajectory \(x\), primitive(dt) approximates the integral
using the trapezoidal rule, with integration step dt. For a SampledTraj, primitive() instead integrates over its existing sample intervals:
derivative() approximates the derivative of a sampled trajectory by finite differences. These numerical operations do not provide guaranteed bounds.
speed = SampledTraj({0.:0, 1.:1, 2.:2})
acceleration = speed.derivative() # numerical derivative
distance = speed.primitive() # trapezoidal integral, p(0)=0
doubled_speed = 2. * speed # pointwise arithmetic
SampledTraj<double> speed({{0.,0}, {1.,1}, {2.,2}});
auto acceleration = speed.derivative(); // numerical derivative
auto distance = speed.primitive(); // trapezoidal integral, p(0)=0
auto doubled_speed = 2. * speed; // pointwise arithmetic
Cartesian products
traj_cart_prod(x1,x2,...) combines scalar or vector sampled trajectories into a vector trajectory. The timestamps must be identical. Use sampled_as() to align trajectories when necessary.
x1 = SampledTraj({0.:0, 1.:1})
x2 = SampledTraj({0.:2, 1.:3})
x = traj_cart_prod(x1, x2)
SampledTraj<double> x1({0.,1}, {0.,1});
SampledTraj<double> x2({0.,1}, {2.,3});
auto x = traj_cart_prod(x1,x2);
Unwrapping periodic signals
continuous_traj() removes apparent jumps in a sampled scalar signal, for instance a heading crossing its angular discontinuity.
heading = SampledTraj({0.:6.1, 1.:0.1, 2.:0.2})
continuous_heading = continuous_traj(heading)
SampledTraj<double> heading({{0.,6.1}, {1.,0.1}, {2.,0.2}});
auto continuous_heading = continuous_traj(heading);
The function detects jumps between consecutive samples and corrects them using a period estimated from the observed values. This is particularly useful for trajectories representing angles. Since the period is estimated rather than specified, the correction may be approximate.
Applying analytic functions
f.traj_eval(x1,x2,...) evaluates an AnalyticFunction on sampled trajectories at the timestamps of the first input, interpolating the other inputs as needed. In Python, inputs are all scalar trajectories or all vector trajectories, while C++ also accepts mixed inputs. For interval enclosures on tubes, see Evaluating analytic functions on tubes.
For example, given a heading \(\theta(t)\) and a speed \(v(t)\), an analytic function computes the Cartesian velocity components:
theta = SampledTraj({0.:0, 1.:0.1, 2.:0.2})
v = SampledTraj({0.:1., 1.:2., 2.:3.})
theta_var, v_var = ScalarVar(), ScalarVar()
f = AnalyticFunction([theta_var, v_var], [v_var*cos(theta_var), v_var*sin(theta_var)])
u = f.traj_eval(theta, v)
SampledTraj<double> theta({{0.,0}, {1.,0.1}, {2.,0.2}});
SampledTraj<double> v({{0.,1}, {1.,2}, {2.,3}});
ScalarVar theta_var, v_var;
AnalyticFunction f({theta_var,v_var}, {v_var*cos(theta_var), v_var*sin(theta_var)});
auto u = f.traj_eval(theta, v);
For a vector-valued input trajectory, use a VectorVar:
p = SampledTraj({0.: Vector([3,4]), 1.: Vector([0,5])})
p_var = VectorVar(2)
f = AnalyticFunction([p_var], sqr(p_var[0]) + sqr(p_var[1]))
result = f.traj_eval(p)
SampledTraj<Vector> p({{0.,{3.,4}}, {1.,{0.,5}}});
VectorVar p_var(2);
AnalyticFunction f({p_var}, sqr(p_var[0]) + sqr(p_var[1]));
auto result = f.traj_eval(p);
Using a trajectory in an analytic expression
as_function() makes a trajectory available inside an analytic expression of time. This is useful when an expression depends on an existing time series.
measurements = SampledTraj({0.:1, 1.:2, 2.:1.5})
measurement_fn = measurements.as_function()
t = ScalarVar()
energy = AnalyticFunction([t], sqr(measurement_fn(t)))
estimated_energy = energy.eval(0.5)
SampledTraj<double> measurements({{0.,1}, {1.,2}, {2.,1.5}});
auto measurement_fn = measurements.as_function();
ScalarVar t;
AnalyticFunction energy({t}, sqr(measurement_fn(t)));
auto estimated_energy = energy.eval(0.5);
The trajectory operator currently supports forward evaluation but not general backward contraction or derivative propagation through the expression.
Converting trajectories and tubes
SlicedTube(td,traj) encloses a trajectory on a temporal partition. To enclose an unknown signal, additional measurement and interpolation uncertainty may be needed. Conversely, tube.mid() gives a numerical midpoint trajectory without retaining the tube’s uncertainty.
t = ScalarVar()
traj = AnalyticTraj([0,2], AnalyticFunction([t],sin(t)))
td = create_tdomain([0,2], 0.1)
tube = SlicedTube(td, traj)
tube.inflate(0.05) # encloses the true signal if its error is bounded by 0.05
ScalarVar t;
AnalyticTraj traj({0,2}, AnalyticFunction({t}, sin(t)));
auto td = create_tdomain({0,2}, 0.1);
SlicedTube tube(td, traj);
tube.inflate(0.05); // encloses the true signal if its error is bounded by 0.05