Skip to content

Commit c91e518

Browse files
Add Python bindings and make Matplot integration optional
This PR adds a pybind11-based pycddp package with bindings for solver options, dynamics, objectives, constraints, and solver execution, along with Python integration tests and packaging metadata. It also makes Matplot-dependent includes and test links optional, adds CMake package installation metadata, and prepares the core library for shared-library consumers such as the Python module.
1 parent a24cfba commit c91e518

34 files changed

Lines changed: 1157 additions & 24 deletions

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ pdf_images/
2121

2222
# Solver output
2323
solver_snopt.out
24+
25+
# Python / uv
26+
.venv/
27+
uv.lock
28+
*.egg-info/
29+
__pycache__/
30+
*.so
31+
*.pyd

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

CMakeLists.txt

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ endif()
4343

4444
# Options
4545
option(CDDP_CPP_BUILD_TESTS "Whether to build tests." ON)
46+
option(CDDP_CPP_BUILD_EXAMPLES "Whether to build examples." ON)
47+
option(CDDP_CPP_ENABLE_PLOTTING "Enable matplotplusplus for visualization." ON)
4648

4749
# CasADi Configuration
4850
option(CDDP_CPP_CASADI "Whether to use CasADi" OFF)
@@ -92,14 +94,16 @@ endif()
9294
# Enable FetchContent for downloading dependencies
9395
include(FetchContent)
9496

95-
# Matplotplusplus
96-
FetchContent_Declare(matplotplusplus
97-
GIT_REPOSITORY https://github.com/alandefreitas/matplotplusplus
98-
GIT_TAG origin/master) # or whatever tag you want
99-
FetchContent_GetProperties(matplotplusplus)
100-
if(NOT matplotplusplus_POPULATED)
101-
FetchContent_Populate(matplotplusplus)
102-
add_subdirectory(${matplotplusplus_SOURCE_DIR} ${matplotplusplus_BINARY_DIR} EXCLUDE_FROM_ALL)
97+
# Matplotplusplus (optional)
98+
if(CDDP_CPP_ENABLE_PLOTTING)
99+
FetchContent_Declare(matplotplusplus
100+
GIT_REPOSITORY https://github.com/alandefreitas/matplotplusplus
101+
GIT_TAG origin/master)
102+
FetchContent_GetProperties(matplotplusplus)
103+
if(NOT matplotplusplus_POPULATED)
104+
FetchContent_Populate(matplotplusplus)
105+
add_subdirectory(${matplotplusplus_SOURCE_DIR} ${matplotplusplus_BINARY_DIR} EXCLUDE_FROM_ALL)
106+
endif()
103107
endif()
104108

105109
# autodiff
@@ -172,16 +176,23 @@ set(dynamics_model_srcs
172176
src/dynamics_model/euler_attitude.cpp
173177
)
174178

175-
add_library(${PROJECT_NAME}
179+
add_library(${PROJECT_NAME}
176180
${cddp_core_srcs}
177181
${dynamics_model_srcs}
178182
)
179183

180-
target_link_libraries(${PROJECT_NAME}
184+
# Enable position-independent code (required for Python shared library linking)
185+
set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
186+
187+
target_link_libraries(${PROJECT_NAME}
181188
$<IF:$<BOOL:${Eigen3_FOUND}>,Eigen3::Eigen,>
182-
matplot
183189
autodiff
184-
)
190+
)
191+
192+
if(CDDP_CPP_ENABLE_PLOTTING)
193+
target_link_libraries(${PROJECT_NAME} matplot)
194+
target_compile_definitions(${PROJECT_NAME} PUBLIC CDDP_HAS_MATPLOT)
195+
endif()
185196

186197
if(NOT Eigen3_FOUND)
187198
target_include_directories(${PROJECT_NAME} PUBLIC ${EIGEN3_INCLUDE_DIRS})
@@ -258,11 +269,53 @@ if (CDDP_CPP_BUILD_TESTS)
258269
endif()
259270

260271
# Build examples
261-
add_subdirectory(examples)
262-
263-
# Cmake compile commmand:
264-
# $ mkdir build
265-
# $ cd build
266-
# $ cmake -DCDDP_CPP_BUILD_TESTS=ON -DCDDP_CPP_CASADI=ON ..
267-
# $ make -j4
268-
# $ make test
272+
if (CDDP_CPP_BUILD_EXAMPLES)
273+
add_subdirectory(examples)
274+
endif()
275+
276+
# Python bindings (optional)
277+
option(CDDP_CPP_BUILD_PYTHON "Build Python bindings" OFF)
278+
if(CDDP_CPP_BUILD_PYTHON)
279+
add_subdirectory(python)
280+
endif()
281+
282+
# Install targets
283+
include(GNUInstallDirs)
284+
include(CMakePackageConfigHelpers)
285+
286+
install(TARGETS ${PROJECT_NAME}
287+
EXPORT cddpTargets
288+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
289+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
290+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
291+
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
292+
)
293+
294+
install(DIRECTORY include/cddp-cpp/
295+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/cddp-cpp
296+
FILES_MATCHING PATTERN "*.hpp"
297+
)
298+
299+
install(EXPORT cddpTargets
300+
FILE cddpTargets.cmake
301+
NAMESPACE cddp::
302+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cddp
303+
)
304+
305+
configure_package_config_file(
306+
${CMAKE_CURRENT_SOURCE_DIR}/cmake/cddpConfig.cmake.in
307+
${CMAKE_CURRENT_BINARY_DIR}/cddpConfig.cmake
308+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cddp
309+
)
310+
311+
write_basic_package_version_file(
312+
${CMAKE_CURRENT_BINARY_DIR}/cddpConfigVersion.cmake
313+
VERSION ${PROJECT_VERSION}
314+
COMPATIBILITY SameMajorVersion
315+
)
316+
317+
install(FILES
318+
${CMAKE_CURRENT_BINARY_DIR}/cddpConfig.cmake
319+
${CMAKE_CURRENT_BINARY_DIR}/cddpConfigVersion.cmake
320+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cddp
321+
)

cmake/cddpConfig.cmake.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@PACKAGE_INIT@
2+
3+
include(CMakeFindDependencyMacro)
4+
find_dependency(Eigen3)
5+
6+
include("${CMAKE_CURRENT_LIST_DIR}/cddpTargets.cmake")
7+
check_required_components(cddp)

include/cddp-cpp/cddp.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
#include "dynamics_model/quaternion_attitude.hpp"
5858
#include "dynamics_model/mrp_attitude.hpp"
5959

60+
#ifdef CDDP_HAS_MATPLOT
6061
#include "matplot/matplot.h"
62+
#endif
6163

6264
#endif // CDDP_HPP

pyproject.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[build-system]
2+
requires = ["scikit-build-core>=0.8", "pybind11>=2.12"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "pycddp"
7+
version = "0.1.0"
8+
description = "Python bindings for CDDP trajectory optimization"
9+
readme = "README.md"
10+
license = {text = "Apache-2.0"}
11+
requires-python = ">=3.10"
12+
dependencies = ["numpy>=1.22"]
13+
14+
[project.optional-dependencies]
15+
test = ["pytest>=7.0"]
16+
viz = ["matplotlib>=3.5"]
17+
18+
[dependency-groups]
19+
dev = ["pytest>=7.0", "matplotlib>=3.5"]
20+
21+
[tool.scikit-build]
22+
cmake.args = ["-DCDDP_CPP_BUILD_PYTHON=ON", "-DCDDP_CPP_BUILD_TESTS=OFF", "-DCDDP_CPP_BUILD_EXAMPLES=OFF", "-DCDDP_CPP_ENABLE_PLOTTING=OFF"]
23+
wheel.packages = ["python/pycddp"]

python/CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
find_package(pybind11 CONFIG QUIET)
2+
if(NOT pybind11_FOUND)
3+
message(STATUS "pybind11 not found, fetching...")
4+
FetchContent_Declare(
5+
pybind11
6+
GIT_REPOSITORY https://github.com/pybind/pybind11.git
7+
GIT_TAG v2.13.6
8+
)
9+
FetchContent_MakeAvailable(pybind11)
10+
endif()
11+
12+
pybind11_add_module(_pycddp_core
13+
src/main.cpp
14+
src/bind_options.cpp
15+
src/bind_dynamics.cpp
16+
src/bind_objective.cpp
17+
src/bind_constraints.cpp
18+
src/bind_solver.cpp
19+
)
20+
21+
target_link_libraries(_pycddp_core PRIVATE cddp)
22+
target_include_directories(_pycddp_core PRIVATE
23+
${CMAKE_SOURCE_DIR}/include/cddp-cpp
24+
)
25+
26+
install(TARGETS _pycddp_core DESTINATION pycddp)

python/pycddp/__init__.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""pycddp - Python bindings for CDDP trajectory optimization."""
2+
3+
from pycddp._pycddp_core import (
4+
# Enums
5+
SolverType,
6+
BarrierStrategy,
7+
8+
# Options
9+
CDDPOptions,
10+
BoxQPOptions,
11+
LineSearchOptions,
12+
RegularizationOptions,
13+
BarrierOptions,
14+
FilterOptions,
15+
InteriorPointOptions,
16+
LogBarrierOptions,
17+
IPDDPOptions,
18+
MSIPDDPOptions,
19+
20+
# Core solver
21+
CDDP,
22+
CDDPSolution,
23+
SolutionHistory,
24+
25+
# Dynamics base
26+
DynamicalSystem,
27+
28+
# Concrete dynamics models
29+
Pendulum,
30+
Unicycle,
31+
Bicycle,
32+
Car,
33+
CartPole,
34+
DubinsCar,
35+
Forklift,
36+
Acrobot,
37+
Quadrotor,
38+
QuadrotorRate,
39+
Manipulator,
40+
HCW,
41+
SpacecraftLinearFuel,
42+
SpacecraftNonlinear,
43+
DreyfusRocket,
44+
SpacecraftLanding2D,
45+
SpacecraftROE,
46+
SpacecraftTwobody,
47+
LTISystem,
48+
Usv3Dof,
49+
EulerAttitude,
50+
QuaternionAttitude,
51+
MrpAttitude,
52+
53+
# Objectives
54+
Objective,
55+
QuadraticObjective,
56+
NonlinearObjective,
57+
58+
# Constraints
59+
Constraint,
60+
ControlConstraint,
61+
StateConstraint,
62+
LinearConstraint,
63+
BallConstraint,
64+
PoleConstraint,
65+
SecondOrderConeConstraint,
66+
ThrustMagnitudeConstraint,
67+
MaxThrustMagnitudeConstraint,
68+
69+
)
70+
71+
from pycddp._version import __version__
72+
73+
__all__ = [name for name in dir() if not name.startswith("_")]

python/pycddp/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

python/src/bind_constraints.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#include <pybind11/pybind11.h>
2+
#include <pybind11/eigen.h>
3+
#include <pybind11/stl.h>
4+
5+
#include "cddp_core/constraint.hpp"
6+
7+
namespace py = pybind11;
8+
9+
template <typename T>
10+
using nodeleter = std::unique_ptr<T, py::nodelete>;
11+
12+
void bind_constraints(py::module_& m) {
13+
// Base Constraint class - py::nodelete so CDDP can take ownership
14+
py::class_<cddp::Constraint, nodeleter<cddp::Constraint>>(m, "Constraint")
15+
.def("evaluate", &cddp::Constraint::evaluate,
16+
py::arg("state"), py::arg("control"), py::arg("index") = 0)
17+
.def("get_lower_bound", &cddp::Constraint::getLowerBound)
18+
.def("get_upper_bound", &cddp::Constraint::getUpperBound)
19+
.def("get_state_jacobian", &cddp::Constraint::getStateJacobian,
20+
py::arg("state"), py::arg("control"), py::arg("index") = 0)
21+
.def("get_control_jacobian", &cddp::Constraint::getControlJacobian,
22+
py::arg("state"), py::arg("control"), py::arg("index") = 0)
23+
.def("compute_violation", &cddp::Constraint::computeViolation,
24+
py::arg("state"), py::arg("control"), py::arg("index") = 0)
25+
.def("get_dual_dim", &cddp::Constraint::getDualDim)
26+
.def_property_readonly("name", &cddp::Constraint::getName);
27+
28+
py::class_<cddp::ControlConstraint, cddp::Constraint, nodeleter<cddp::ControlConstraint>>(m, "ControlConstraint")
29+
.def(py::init<const Eigen::VectorXd&, const Eigen::VectorXd&, double>(),
30+
py::arg("lower_bound"), py::arg("upper_bound"),
31+
py::arg("scale_factor") = 1.0);
32+
33+
py::class_<cddp::StateConstraint, cddp::Constraint, nodeleter<cddp::StateConstraint>>(m, "StateConstraint")
34+
.def(py::init<const Eigen::VectorXd&, const Eigen::VectorXd&, double>(),
35+
py::arg("lower_bound"), py::arg("upper_bound"),
36+
py::arg("scale_factor") = 1.0);
37+
38+
py::class_<cddp::LinearConstraint, cddp::Constraint, nodeleter<cddp::LinearConstraint>>(m, "LinearConstraint")
39+
.def(py::init<const Eigen::MatrixXd&, const Eigen::VectorXd&, double>(),
40+
py::arg("A"), py::arg("b"), py::arg("scale_factor") = 1.0);
41+
42+
py::class_<cddp::BallConstraint, cddp::Constraint, nodeleter<cddp::BallConstraint>>(m, "BallConstraint")
43+
.def(py::init<double, const Eigen::VectorXd&, double>(),
44+
py::arg("radius"), py::arg("center"),
45+
py::arg("scale_factor") = 1.0)
46+
.def("get_center", &cddp::BallConstraint::getCenter);
47+
48+
py::class_<cddp::PoleConstraint, cddp::Constraint, nodeleter<cddp::PoleConstraint>>(m, "PoleConstraint")
49+
.def(py::init<const Eigen::VectorXd&, char, double, double, double>(),
50+
py::arg("center"), py::arg("direction"),
51+
py::arg("radius"), py::arg("length"),
52+
py::arg("scale_factor") = 1.0);
53+
54+
py::class_<cddp::SecondOrderConeConstraint, cddp::Constraint, nodeleter<cddp::SecondOrderConeConstraint>>(m, "SecondOrderConeConstraint")
55+
.def(py::init<const Eigen::Vector3d&, const Eigen::Vector3d&, double, double, const std::string&>(),
56+
py::arg("cone_origin"), py::arg("opening_direction"),
57+
py::arg("cone_angle_fov"), py::arg("epsilon") = 1e-6,
58+
py::arg("name") = "SecondOrderConeConstraint");
59+
60+
py::class_<cddp::ThrustMagnitudeConstraint, cddp::Constraint, nodeleter<cddp::ThrustMagnitudeConstraint>>(m, "ThrustMagnitudeConstraint")
61+
.def(py::init<double, double, double>(),
62+
py::arg("min_thrust"), py::arg("max_thrust"),
63+
py::arg("epsilon") = 1e-6);
64+
65+
py::class_<cddp::MaxThrustMagnitudeConstraint, cddp::Constraint, nodeleter<cddp::MaxThrustMagnitudeConstraint>>(m, "MaxThrustMagnitudeConstraint")
66+
.def(py::init<double, double>(),
67+
py::arg("max_thrust"), py::arg("epsilon") = 1e-6);
68+
}

0 commit comments

Comments
 (0)