copp 0.2.2

Convex-objective path parameterization for robotic trajectory planning.
Documentation
import numpy as np
import pytest

import copp_py as copp


class PolynomialEvaluator:
    dim = 2

    def evaluate_q(self, s):
        q = np.empty((s.size, self.dim), dtype=np.float64)
        q[:, 0] = s**3
        q[:, 1] = s**2 + 1.0
        return q

    def evaluate_up_to_2nd(self, s):
        q = self.evaluate_q(s)

        dq = np.empty_like(q)
        dq[:, 0] = 3.0 * s**2
        dq[:, 1] = 2.0 * s

        ddq = np.empty_like(q)
        ddq[:, 0] = 6.0 * s
        ddq[:, 1] = 2.0

        return q, dq, ddq

    def evaluate_up_to_3rd(self, s):
        q, dq, ddq = self.evaluate_up_to_2nd(s)

        dddq = np.empty_like(q)
        dddq[:, 0] = 6.0
        dddq[:, 1] = 0.0

        return q, dq, ddq, dddq


def test_path_from_evaluator_3rd_sample_major_smoke():
    path = copp.Path.from_evaluator_3rd(PolynomialEvaluator(), -1.0, 1.0)
    s = np.array([-1.0, 0.0, 0.5], dtype=np.float64)

    out = path.evaluate_up_to_3rd(s)

    assert path.dim == 2
    assert path.s_range == (-1.0, 1.0)
    assert out.q.shape == (3, 2)
    assert out.dq.shape == (3, 2)
    assert out.ddq.shape == (3, 2)
    assert out.dddq.shape == (3, 2)
    assert np.allclose(out.q[:, 0], s**3)
    assert np.allclose(out.dq[:, 0], 3.0 * s**2)
    assert np.allclose(out.ddq[:, 0], 6.0 * s)
    assert np.allclose(out.dddq[:, 0], 6.0)


def test_path_from_evaluator_2nd_rejects_third_order_evaluation():
    path = copp.Path.from_evaluator_2nd(PolynomialEvaluator(), -1.0, 1.0)
    s = np.array([-1.0, 0.0, 0.5], dtype=np.float64)

    out = path.evaluate_up_to_2nd(s)

    assert out.q.shape == (3, 2)
    assert out.dq.shape == (3, 2)
    assert out.ddq.shape == (3, 2)
    assert out.dddq is None

    with pytest.raises(copp.CoppError, match="unsupported derivative order"):
        path.evaluate_up_to_3rd(s)


def test_path_from_evaluator_3rd_can_fallback_for_second_order():
    class ThirdOrderOnly:
        dim = 1

        def __init__(self):
            self.calls_3rd = 0

        def evaluate_up_to_3rd(self, s):
            self.calls_3rd += 1
            q = np.ascontiguousarray((s**2)[:, None], dtype=np.float64)
            dq = np.ascontiguousarray((2.0 * s)[:, None], dtype=np.float64)
            ddq = np.full((s.size, 1), 2.0, dtype=np.float64)
            dddq = np.zeros((s.size, 1), dtype=np.float64)
            return q, dq, ddq, dddq

    evaluator = ThirdOrderOnly()
    path = copp.Path.from_evaluator_3rd(evaluator, -1.0, 1.0)
    s = np.array([-0.5, 0.0, 0.5], dtype=np.float64)

    out = path.evaluate_up_to_2nd(s)

    assert evaluator.calls_3rd == 1
    assert out.q.shape == (3, 1)
    assert out.dddq is None
    assert np.allclose(out.q[:, 0], s**2)


def test_path_evaluator_preserves_python_callback_exception():
    class BrokenEvaluator:
        dim = 1

        def evaluate_up_to_2nd(self, s):
            raise RuntimeError("callback failed")

    path = copp.Path.from_evaluator_2nd(BrokenEvaluator(), 0.0, 1.0)
    s = np.array([0.0, 0.5], dtype=np.float64)

    with pytest.raises(RuntimeError, match="callback failed"):
        path.evaluate_up_to_2nd(s)


def test_path_evaluator_invalid_callback_shape_is_value_error():
    class BadShapeEvaluator:
        dim = 1

        def evaluate_up_to_2nd(self, s):
            bad = np.zeros((s.size, 2), dtype=np.float64)
            return bad, bad, bad

    path = copp.Path.from_evaluator_2nd(BadShapeEvaluator(), 0.0, 1.0)
    s = np.array([0.0, 0.5], dtype=np.float64)

    with pytest.raises(ValueError, match="expected shape"):
        path.evaluate_up_to_2nd(s)


def test_path_evaluator_outputs_accept_arraylike():
    class ListEvaluator:
        dim = 1

        def evaluate_up_to_2nd(self, s):
            values = [[float(x)] for x in s]
            zeros = [[0.0] for _ in s]
            return values, values, zeros

    path = copp.Path.from_evaluator_2nd(ListEvaluator(), 0.0, 1.0)
    out = path.evaluate_up_to_2nd([0.0, 0.5, 1.0])

    assert out.q.shape == (3, 1)
    assert np.allclose(out.q[:, 0], [0.0, 0.5, 1.0])