classdef test_path_parametric < matlab.unittest.TestCase
%TEST_PATH_PARAMETRIC Public examples for formula-backed Path objects.
%
% These tests cover optional MATLAB parametric constructors that turn
% symbolic formulas or CasADi expressions into third-order batch evaluator
% paths. Each optional dependency is checked with an assumption so a
% machine without Symbolic Math Toolbox or CasADi reports an incomplete
% smoke test instead of failing the core MATLAB binding suite.
methods (Test)
function jet3_formula_path_evaluates_through_third_order(testCase)
% Pure MATLAB formulas use copp.Jet3 and need no toolbox.
path = copp.Path.from_parametric( ...
@(s) [ ...
sin(s); ...
cos(2*s); ...
s + 0.1*s^2; ...
sqrt(s + 1) + exp(s) - log(s + 1); ...
3], ...
s_range=[0, 1]);
cleaner = onCleanup(@() path.release());
samples = [0, 0.5, 1.0];
[q, dq, ddq, dddq] = path.evaluate_up_to_3rd(samples);
expected_q = [ ...
sin(samples); ...
cos(2*samples); ...
samples + 0.1 .* samples.^2; ...
sqrt(samples + 1) + exp(samples) - log(samples + 1); ...
3.0 .* ones(size(samples))];
expected_dq = [ ...
cos(samples); ...
-2.0 .* sin(2*samples); ...
1.0 + 0.2 .* samples; ...
0.5 ./ sqrt(samples + 1) + exp(samples) - 1 ./ (samples + 1); ...
zeros(size(samples))];
expected_ddq = [ ...
-sin(samples); ...
-4.0 .* cos(2*samples); ...
0.2 .* ones(size(samples)); ...
-0.25 ./ (samples + 1).^(1.5) + exp(samples) + 1 ./ (samples + 1).^2; ...
zeros(size(samples))];
expected_dddq = [ ...
-cos(samples); ...
8.0 .* sin(2*samples); ...
zeros(size(samples)); ...
0.375 ./ (samples + 1).^(2.5) + exp(samples) - 2 ./ (samples + 1).^3; ...
zeros(size(samples))];
testCase.verifyEqual(path.dim, 5);
testCase.verifyEqual(path.s_range, [0, 1], AbsTol=1e-12);
testCase.verifyEqual(q, expected_q, AbsTol=1e-10);
testCase.verifyEqual(dq, expected_dq, AbsTol=1e-10);
testCase.verifyEqual(ddq, expected_ddq, AbsTol=1e-10);
testCase.verifyEqual(dddq, expected_dddq, AbsTol=1e-10);
explicit = copp.Path.from_parametric(@(s) [s; s.^2], dim=2, s_range=[-1, 1]);
explicit_cleaner = onCleanup(@() explicit.release());
explicit_out = explicit.evaluate_up_to_3rd([-1, 0, 1]);
testCase.verifyEqual(explicit_out.q, [-1, 0, 1; 1, 0, 1], AbsTol=1e-12);
testCase.verifyEqual(explicit_out.dddq, zeros(2, 3), AbsTol=1e-12);
testCase.verifyError( ...
@() copp.Path.from_parametric(@(s) [s; s^2], dim=3), ...
'copp:InvalidArgument');
testCase.verifyError( ...
@() copp.Path.from_parametric(@(s) [s, s; s, s]), ...
'copp:InvalidArgument');
clear explicit_cleaner
clear cleaner
end
function symbolic_formula_path_evaluates_through_third_order(testCase)
% Symbolic Math Toolbox formulas can be differentiated internally.
testCase.assumeTrue( ...
has_symbolic_toolbox(), ...
"Symbolic Math Toolbox is not available.");
syms s t
q_expr = [ ...
sin(s); ...
cos(2*s); ...
s + sym(1)/10*s^2; ...
sym(3)];
path = copp.Path.from_symbolic(q_expr, symbol=s, s_range=[0, 1]);
cleaner = onCleanup(@() path.release());
samples = [0, 0.5, 1.0];
[q, dq, ddq, dddq] = path.evaluate_up_to_3rd(samples);
expected_q = [ ...
sin(samples); ...
cos(2*samples); ...
samples + 0.1 .* samples.^2; ...
3.0 .* ones(size(samples))];
expected_dq = [ ...
cos(samples); ...
-2.0 .* sin(2*samples); ...
1.0 + 0.2 .* samples; ...
zeros(size(samples))];
expected_ddq = [ ...
-sin(samples); ...
-4.0 .* cos(2*samples); ...
0.2 .* ones(size(samples)); ...
zeros(size(samples))];
expected_dddq = [ ...
-cos(samples); ...
8.0 .* sin(2*samples); ...
zeros(size(samples)); ...
zeros(size(samples))];
testCase.verifyEqual(path.dim, 4);
testCase.verifyEqual(path.s_range, [0, 1], AbsTol=1e-12);
testCase.verifyEqual(q, expected_q, AbsTol=1e-10);
testCase.verifyEqual(dq, expected_dq, AbsTol=1e-10);
testCase.verifyEqual(ddq, expected_ddq, AbsTol=1e-10);
testCase.verifyEqual(dddq, expected_dddq, AbsTol=1e-10);
% With exactly one symbolic variable, symbol= may be inferred.
inferred_path = copp.Path.from_symbolic([s; s^2], s_range=[-1, 1]);
inferred_cleaner = onCleanup(@() inferred_path.release());
inferred = inferred_path.evaluate_up_to_2nd([-1, 0, 1]);
testCase.verifyEqual(inferred.q, [-1, 0, 1; 1, 0, 1], AbsTol=1e-12);
% Multiple variables require the user to choose the path parameter.
testCase.verifyError( ...
@() copp.Path.from_symbolic([s + t; s], s_range=[0, 1]), ...
'copp:InvalidArgument');
clear inferred_cleaner
clear cleaner
end
function casadi_formula_path_evaluates_through_third_order(testCase)
% CasADi expressions can also be differentiated and batch-evaluated.
testCase.assumeTrue( ...
has_casadi(), ...
"CasADi MATLAB package is not available.");
import casadi.*
s = SX.sym('s');
q_expr = [ ...
sin(s); ...
cos(2*s); ...
s + 0.1*s^2; ...
s - s + 3.0];
path = copp.Path.from_casadi(q_expr, symbol=s, s_range=[0, 1]);
cleaner = onCleanup(@() path.release());
samples = [0, 0.5, 1.0];
[q, dq, ddq, dddq] = path.evaluate_up_to_3rd(samples);
expected_q = [ ...
sin(samples); ...
cos(2*samples); ...
samples + 0.1 .* samples.^2; ...
3.0 .* ones(size(samples))];
expected_dq = [ ...
cos(samples); ...
-2.0 .* sin(2*samples); ...
1.0 + 0.2 .* samples; ...
zeros(size(samples))];
expected_ddq = [ ...
-sin(samples); ...
-4.0 .* cos(2*samples); ...
0.2 .* ones(size(samples)); ...
zeros(size(samples))];
expected_dddq = [ ...
-cos(samples); ...
8.0 .* sin(2*samples); ...
zeros(size(samples)); ...
zeros(size(samples))];
testCase.verifyEqual(path.dim, 4);
testCase.verifyEqual(q, expected_q, AbsTol=1e-10);
testCase.verifyEqual(dq, expected_dq, AbsTol=1e-10);
testCase.verifyEqual(ddq, expected_ddq, AbsTol=1e-10);
testCase.verifyEqual(dddq, expected_dddq, AbsTol=1e-10);
% CasADi does not support safe symbol inference here; symbol= is
% required so q_expr can be differentiated against the right scalar.
testCase.verifyError( ...
@() copp.Path.from_casadi(q_expr, s_range=[0, 1]), ...
'copp:InvalidArgument');
clear cleaner
end
end
end
function tf = has_symbolic_toolbox()
%HAS_SYMBOLIC_TOOLBOX Return true when Symbolic Math Toolbox is visible.
tf = exist('sym', 'class') == 8 || exist('sym', 'file') == 2;
end
function tf = has_casadi()
%HAS_CASADI Return true when the CasADi MATLAB package is visible.
tf = exist('casadi.Function', 'class') == 8 || ...
exist('casadi.Function', 'file') == 2;
end