copp 0.2.2

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

import copp_py as copp


def test_path_from_jax_smoke():
    jax = pytest.importorskip("jax")
    jnp = pytest.importorskip("jax.numpy")
    jax.config.update("jax_enable_x64", True)

    def q_fn(s):
        return jnp.array([jnp.sin(s), jnp.cos(s), s + 0.1 * s**2])

    path = copp.Path.from_jax(
        q_fn,
        0.0,
        1.0,
        jit=False,
        require_x64=True,
    )
    s = np.array([0.0, 0.5, 1.0], dtype=np.float64)

    out = path.evaluate_up_to_3rd(s)

    assert path.dim == 3
    assert out.q.shape == (3, 3)
    assert out.dq.shape == (3, 3)
    assert out.ddq.shape == (3, 3)
    assert out.dddq.shape == (3, 3)
    assert np.allclose(out.q[:, 0], np.sin(s))
    assert np.allclose(out.dq[:, 2], 1.0 + 0.2 * s)
    assert np.allclose(out.ddq[:, 2], 0.2)
    assert np.allclose(out.dddq[:, 2], 0.0)


def test_path_from_autograd_smoke():
    pytest.importorskip("autograd")
    anp = pytest.importorskip("autograd.numpy")

    def q_fn(s):
        return anp.array([anp.sin(s), anp.cos(s), s + 0.1 * s**2])

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

    out = path.evaluate_up_to_3rd(s)

    assert path.dim == 3
    assert np.allclose(out.q[:, 1], np.cos(s))
    assert np.allclose(out.dq[:, 2], 1.0 + 0.2 * s)
    assert np.allclose(out.ddq[:, 2], 0.2)
    assert np.allclose(out.dddq[:, 2], 0.0)


def test_path_from_casadi_smoke():
    ca = pytest.importorskip("casadi")

    s_sym = ca.SX.sym("s")
    q_expr = ca.vertcat(ca.sin(s_sym), ca.cos(s_sym), s_sym + 0.1 * s_sym**2)

    path = copp.Path.from_casadi(q_expr, symbol=s_sym, s_min=0.0, s_max=1.0)
    s = np.array([0.0, 0.5, 1.0], dtype=np.float64)

    out = path.evaluate_up_to_3rd(s)

    assert path.dim == 3
    assert np.allclose(out.q[:, 0], np.sin(s))
    assert np.allclose(out.dq[:, 2], 1.0 + 0.2 * s)
    assert np.allclose(out.ddq[:, 2], 0.2)
    assert np.allclose(out.dddq[:, 2], 0.0)


def test_path_from_sympy_smoke():
    sp = pytest.importorskip("sympy")

    s_sym = sp.symbols("s")
    q_exprs = [
        sp.sin(s_sym),
        sp.cos(s_sym),
        s_sym + sp.Rational(1, 10) * s_sym**2,
    ]

    path = copp.Path.from_sympy(q_exprs, symbol=s_sym, s_min=0.0, s_max=1.0)
    s = np.array([0.0, 0.5, 1.0], dtype=np.float64)

    out = path.evaluate_up_to_3rd(s)

    assert path.dim == 3
    assert np.allclose(out.q[:, 1], np.cos(s))
    assert np.allclose(out.dq[:, 2], 1.0 + 0.2 * s)
    assert np.allclose(out.ddq[:, 2], 0.2)
    assert np.allclose(out.dddq[:, 2], 0.0)


def test_path_from_jax_reports_missing_backend_dependency(monkeypatch):
    import copp_py._parametric as parametric

    def fake_import_optional(module_name, message):
        if module_name == "jax":
            raise ImportError(message)
        return __import__(module_name, fromlist=["*"])

    monkeypatch.setattr(parametric, "_import_optional", fake_import_optional)

    with pytest.raises(ImportError, match=r"copp-py\[jax\]"):
        copp.Path.from_jax(lambda s: [s], 0.0, 1.0)