copp 0.2.2

Convex-objective path parameterization for robotic trajectory planning.
Documentation
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