copp 0.2.2

Convex-objective path parameterization for robotic trajectory planning.
Documentation
classdef test_path_evaluator < matlab.unittest.TestCase
    %TEST_PATH_EVALUATOR Public examples for callback-backed Path objects.
    %
    % These tests intentionally use only copp.Path public APIs. They show
    % the two supported MATLAB evaluator styles:
    %   1. a function handle, useful for compact scripts and demos;
    %   2. an object evaluator, useful when the path owns parameters or state.
    %
    % MATLAB callback inputs are always 1-by-N row vectors. Callback outputs
    % are dim-by-N matrices: one column per path sample.

    methods (Test)
        function function_handle_second_order_evaluator(testCase)
            % A function-handle evaluator returns q, dq, and ddq.
            path = copp.Path.from_evaluator_2nd( ...
                @quadratic_2nd, ...
                dim=2, ...
                s_range=[-1, 1]);
            cleaner = onCleanup(@() path.release());

            % Users may pass row or column s vectors into evaluate_* methods.
            % The native callback receives a normalized row vector.
            s = [-1, 0, 0.5];
            [q, dq, ddq] = path.evaluate_up_to_2nd(s);

            expected_q = [s; s.^2 + 1.0];
            expected_dq = [ones(size(s)); 2.0 .* s];
            expected_ddq = [zeros(size(s)); 2.0 .* ones(size(s))];

            testCase.verifyEqual(path.dim, 2);
            testCase.verifyEqual(path.s_range, [-1, 1], AbsTol=1e-12);
            testCase.verifyEqual(q, expected_q, AbsTol=1e-12);
            testCase.verifyEqual(dq, expected_dq, AbsTol=1e-12);
            testCase.verifyEqual(ddq, expected_ddq, AbsTol=1e-12);

            % One-output form returns a struct with named derivative fields.
            out = path.evaluate_up_to_2nd(s.');
            testCase.verifyEqual(out.q, expected_q, AbsTol=1e-12);
            testCase.verifyEqual(out.dq, expected_dq, AbsTol=1e-12);
            testCase.verifyEqual(out.ddq, expected_ddq, AbsTol=1e-12);

            % A second-order evaluator path deliberately does not support
            % third-order evaluation.
            testCase.verifyError( ...
                @() path.evaluate_up_to_3rd(s), ...
                'copp:PathError');

            clear cleaner
            testCase.verifyFalse(path.is_valid());
        end

        function object_third_order_evaluator_uses_optional_second_order_method(testCase)
            % An object evaluator can keep parameters and counters as state.
            test_dir = fileparts(mfilename('fullpath'));
            path_cleaner = onCleanup(@() rmpath(test_dir));
            addpath(test_dir);

            evaluator = QuadraticPathEvaluator();
            path = copp.Path.from_evaluator_3rd( ...
                evaluator, ...
                dim=2, ...
                s_range=[0, 1]);
            native_cleaner = onCleanup(@() path.release());

            s = [0, 0.25, 1.0];
            [q, dq, ddq, dddq] = path.evaluate_up_to_3rd(s);

            testCase.verifyEqual(evaluator.calls_3rd, 1);
            testCase.verifyEqual(q(1, :), s.^3, AbsTol=1e-12);
            testCase.verifyEqual(dq(1, :), 3.0 .* s.^2, AbsTol=1e-12);
            testCase.verifyEqual(ddq(1, :), 6.0 .* s, AbsTol=1e-12);
            testCase.verifyEqual(dddq(1, :), 6.0 .* ones(size(s)), AbsTol=1e-12);

            % Because the object also implements evaluate_up_to_2nd, the
            % native path uses that lighter callback for second-order queries.
            out = path.evaluate_up_to_2nd(s);
            testCase.verifyEqual(evaluator.calls_2nd, 1);
            testCase.verifyEqual(evaluator.calls_3rd, 1);
            testCase.verifyEqual(out.q, q, AbsTol=1e-12);

            clear native_cleaner
            clear path_cleaner
        end

        function function_handle_third_order_falls_back_for_second_order(testCase)
            % When a third-order function handle is evaluated only up to 2nd
            % order, the native path calls the third-order callback and drops
            % dddq internally.
            path = copp.Path.from_evaluator_3rd( ...
                @quadratic_3rd, ...
                dim=2, ...
                s_range=[-1, 1]);
            cleaner = onCleanup(@() path.release());

            s = [-0.5, 0.0, 0.5];
            out = path.evaluate_up_to_2nd(s);

            testCase.verifyEqual(out.q(1, :), s.^3, AbsTol=1e-12);
            testCase.verifyEqual(out.dq(1, :), 3.0 .* s.^2, AbsTol=1e-12);
            testCase.verifyEqual(out.ddq(1, :), 6.0 .* s, AbsTol=1e-12);

            clear cleaner
        end
    end
end

function [q, dq, ddq] = quadratic_2nd(s)
%QUADRATIC_2ND Two-dimensional second-order path evaluator for tests.
assert(isrow(s), 'test_path_evaluator:ExpectedRowS', 'callback input s must be a row vector');
q = [s; s.^2 + 1.0];
dq = [ones(size(s)); 2.0 .* s];
ddq = [zeros(size(s)); 2.0 .* ones(size(s))];
end

function [q, dq, ddq, dddq] = quadratic_3rd(s)
%QUADRATIC_3RD Two-dimensional third-order path evaluator for tests.
assert(isrow(s), 'test_path_evaluator:ExpectedRowS', 'callback input s must be a row vector');
q = [s.^3; s.^2 + 1.0];
dq = [3.0 .* s.^2; 2.0 .* s];
ddq = [6.0 .* s; 2.0 .* ones(size(s))];
dddq = [6.0 .* ones(size(s)); zeros(size(s))];
end