diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..6967b19 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,58 @@ +name: Docs + +on: + push: + branches: [master] + paths: + - "docs/**" + - "mkdocs.yml" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: python -m pip install uv + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Build site + run: uvx --from mkdocs --with mkdocs-material mkdocs build --strict + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + name: Deploy docs + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a11956d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,91 @@ +name: Publish PyPI + +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + build-sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build frontend + run: python -m pip install build + + - name: Build source distribution + run: python -m build --sdist + + - name: Upload source distribution + uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: dist/*.tar.gz + + build-wheels: + name: Build wheels (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-13, macos-14] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install cibuildwheel + run: python -m pip install cibuildwheel + + - name: Build wheels + run: python -m cibuildwheel --output-dir wheelhouse + env: + CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* + CIBW_SKIP: *-musllinux_* *-win32 + CIBW_ARCHS_LINUX: auto64 + CIBW_ARCHS_WINDOWS: auto64 + CIBW_ARCHS_MACOS: auto64 + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-wheels-${{ matrix.os }} + path: wheelhouse/*.whl + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [build-sdist, build-wheels] + permissions: + id-token: write + contents: read + environment: + name: pypi + url: https://pypi.org/p/pycddp + + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + + - name: Publish distributions + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..f7629ee --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,70 @@ +name: Wheel CI + +on: + pull_request: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build frontend + run: python -m pip install build + + - name: Build source distribution + run: python -m build --sdist + + - name: Upload source distribution + uses: actions/upload-artifact@v4 + with: + name: dist-sdist + path: dist/*.tar.gz + + build-wheels: + name: Build wheels (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-13, macos-14] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install cibuildwheel + run: python -m pip install cibuildwheel + + - name: Build wheels + run: python -m cibuildwheel --output-dir wheelhouse + env: + CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* + CIBW_SKIP: *-musllinux_* *-win32 + CIBW_ARCHS_LINUX: auto64 + CIBW_ARCHS_WINDOWS: auto64 + CIBW_ARCHS_MACOS: auto64 + + - name: Upload wheel artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-wheels-${{ matrix.os }} + path: wheelhouse/*.whl diff --git a/.gitignore b/.gitignore index 243ac7e..f58f9ed 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ solver_snopt.out # Python / uv .venv/ uv.lock +site/ *.egg-info/ __pycache__/ *.so diff --git a/CMakeLists.txt b/CMakeLists.txt index 943047c..79b2547 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.15) # Set policy to support older CMake code cmake_policy(SET CMP0048 NEW) # Allow policy version minimum flag to be passed to sub-projects -set(CMAKE_POLICY_VERSION_MINIMUM "3.5" CACHE STRING "Minimum CMake version for policy compatibility") +set(CMAKE_POLICY_VERSION_MINIMUM "3.15" CACHE STRING "Minimum CMake version for policy compatibility") project( cddp @@ -26,6 +26,7 @@ project( ) include(GNUInstallDirs) +include(FetchContent) set(CMAKE_CXX_STANDARD 17) # Enforce C++17 as the minimum standard set(CMAKE_CXX_STANDARD_REQUIRED ON) # Enforce C++17 as the minimum standard @@ -92,9 +93,6 @@ if (CDDP_CPP_CASADI) endif() endif() -# Enable FetchContent for downloading dependencies -include(FetchContent) - # autodiff set(AUTODIFF_BUILD_TESTS OFF CACHE BOOL "Don't build autodiff tests") set(AUTODIFF_BUILD_EXAMPLES OFF CACHE BOOL "Don't build autodiff examples") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8099370..6abcc7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,12 @@ source .venv/bin/activate pytest -q python/tests ``` +### Docs preview + +```bash +uvx --from mkdocs --with mkdocs-material mkdocs serve +``` + ## Pull requests Before opening a pull request: @@ -50,6 +56,9 @@ Before opening a pull request: 4. Update documentation when user-facing behavior, examples, or public APIs change. 5. Write a clear commit message and PR description. +Tagged releases publish Python artifacts to PyPI via GitHub Actions. The +project site deploys from `master` to GitHub Pages. + PRs that include a minimal reproduction, exact validation commands, and a concise explanation of the design tradeoffs are much easier to review. ## Issues diff --git a/README.md b/README.md index 5cb6d3b..00c3231 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,18 @@ sudo apt-get install libeigen3-dev # For Ubuntu brew install eigen # For macOS ``` -### Building +### Python package +Tagged releases publish the Python bindings to PyPI: + +```bash +pip install pycddp +``` + +Prebuilt wheels are intended for CPython 3.10-3.13 on Linux, macOS, and +Windows. If a wheel is not available for your platform yet, install from +source using the steps below. + +### Building from source ```bash git clone https://github.com/astomodynamics/cddp-cpp cd cddp-cpp @@ -105,6 +116,13 @@ make -j4 make test ``` +### Documentation +The project site is published through GitHub Pages at: + + + +The Pages workflow builds the Markdown docs from `docs/` using MkDocs. + ## ROS If you want to use this library for ROS2 MPC node, please refer [CDDP MPC Package](https://github.com/astomodynamics/cddp_mpc). You do not need to install this library to use the package. MPC package will automatically install this library as a dependency. diff --git a/docs/cpp.md b/docs/cpp.md new file mode 100644 index 0000000..4b5a46b --- /dev/null +++ b/docs/cpp.md @@ -0,0 +1,32 @@ +# C++ build + +The repository remains a first-class C++ library even when distributed through +PyPI for Python users. + +## Core build + +```bash +cmake -S . -B build +cmake --build build -j4 +ctest --test-dir build --output-on-failure +``` + +## Python bindings from CMake + +```bash +cmake -S . -B build-python -DCDDP_CPP_BUILD_PYTHON=ON -DCDDP_CPP_BUILD_TESTS=OFF -DCDDP_CPP_BUILD_EXAMPLES=OFF +cmake --build build-python -j4 +``` + +## Installed C++ assets + +The install rules export: + +- `libcddp` and CMake package metadata +- public headers under `include/cddp-cpp/` +- the Python extension when `CDDP_CPP_BUILD_PYTHON=ON` + +That lets the repository serve both as: + +- a source build for C++ consumers +- the native backend for the `pycddp` wheel diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..34fa970 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +# CDDP + +`cddp-cpp` is a constrained differential dynamic programming solver library with: + +- a C++17 core library for trajectory optimization and MPC +- `pycddp` Python bindings built with `pybind11` +- a small animation-oriented Python portfolio for demos and regression checks + +Use the navigation to get started with installation, local development, and +the release workflow for PyPI and GitHub Pages. + +## Project scope + +- C++ library: reusable solver and dynamical-system implementations +- Python package: importable bindings distributed as `pycddp` +- Docs site: Markdown content from `docs/`, published to GitHub Pages + +## Quick start + +Install the Python package from PyPI: + +```bash +pip install pycddp +``` + +Or build from source: + +```bash +git clone https://github.com/astomodynamics/cddp-cpp +cd cddp-cpp +cmake -S . -B build +cmake --build build -j4 +ctest --test-dir build --output-on-failure +``` diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..5a1d3c0 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,57 @@ +# Installation + +## Python users + +Install the published wheel from PyPI: + +```bash +pip install pycddp +``` + +Target release support is: + +- CPython 3.10-3.13 +- Linux x86_64 +- macOS x86_64 and arm64 +- Windows x86_64 + +If your platform does not have a wheel yet, `pip` will fall back to the source +distribution and build locally. + +## Source build prerequisites + +- CMake 3.15+ +- a C++17 compiler +- Eigen 3 +- Python 3.10+ when building the bindings + +Ubuntu example: + +```bash +sudo apt-get install build-essential cmake libeigen3-dev +``` + +macOS example: + +```bash +brew install cmake eigen +``` + +## Local developer install + +For repository work: + +```bash +uv venv .venv --python 3.12 +uv sync +source .venv/bin/activate +pytest -q python/tests +``` + +## Build the Python wheel locally + +```bash +uv build +``` + +This creates `dist/*.tar.gz` and `dist/*.whl`. diff --git a/docs/python.md b/docs/python.md new file mode 100644 index 0000000..bc9d000 --- /dev/null +++ b/docs/python.md @@ -0,0 +1,39 @@ +# Python package + +The Python package is published as `pycddp`. + +## What is included + +- the native extension module `_pycddp_core` +- the public package namespace `pycddp` +- version metadata from `pycddp._version` + +## Local validation + +Run the Python tests after syncing the environment: + +```bash +source .venv/bin/activate +pytest -q python/tests +``` + +Build an installable artifact: + +```bash +uv build +``` + +Smoke-test the wheel in a fresh environment: + +```bash +uv venv /tmp/pycddp-smoke --python 3.13 +source /tmp/pycddp-smoke/bin/activate +uv pip install dist/*.whl +python -c "import pycddp; print(pycddp.__version__)" +``` + +## Release strategy + +- pull requests and pushes validate wheel builds in GitHub Actions +- tagged releases build wheels plus an sdist and publish to PyPI +- publishing uses PyPI Trusted Publishing instead of a long-lived API token diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..d48cde0 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,48 @@ +# Release operations + +This repository now has three distinct automation paths: + +- `wheels.yml`: validates wheel and sdist builds on pull requests and pushes +- `publish.yml`: builds release artifacts from a Git tag and publishes to PyPI +- `pages.yml`: publishes the documentation site to GitHub Pages + +## One-time PyPI setup + +Before the first release, configure PyPI Trusted Publishing for project +`pycddp`. + +On PyPI: + +1. Create the project if it does not exist yet. +2. Add a trusted publisher for GitHub Actions. +3. Set owner to `astomodynamics`. +4. Set repository to `cddp-cpp`. +5. Set workflow to `.github/workflows/publish.yml`. +6. Set environment name to `pypi`. + +After that, pushing a tag like `v0.1.0` will publish the matching artifacts. + +## One-time GitHub Pages setup + +On GitHub: + +1. Open repository settings. +2. Open Pages. +3. Set the source to `GitHub Actions`. + +After that, pushes to `master` that touch the docs or `mkdocs.yml` will deploy +the site. + +## Release steps + +1. Update package version if needed. +2. Merge the release changes to `master`. +3. Create and push a tag: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +4. Verify the `publish.yml` workflow succeeds. +5. Confirm the release appears on PyPI and the docs site is current. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0db7274 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,29 @@ +site_name: CDDP +site_description: Constrained differential dynamic programming in C++ with Python bindings. +site_url: https://astomodynamics.github.io/cddp-cpp/ +repo_url: https://github.com/astomodynamics/cddp-cpp +repo_name: astomodynamics/cddp-cpp + +theme: + name: material + features: + - navigation.instant + - navigation.sections + - navigation.top + - content.code.copy + +plugins: + - search + +markdown_extensions: + - admonition + - pymdownx.highlight + - pymdownx.superfences + +nav: + - Overview: index.md + - Installation: install.md + - Python API: python.md + - C++ Build: cpp.md + - Portfolio: python_portfolio.md + - Release Ops: release.md diff --git a/pyproject.toml b/pyproject.toml index 57e2b2e..38cf25b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,34 @@ name = "pycddp" version = "0.1.0" description = "Python bindings for CDDP trajectory optimization" readme = "README.md" -license = {text = "Apache-2.0"} +authors = [{name = "Tomo Sasaki"}] +license = "Apache-2.0" +license-files = ["LICENSE"] requires-python = ">=3.10" +keywords = ["trajectory-optimization", "model-predictive-control", "differential-dynamic-programming", "optimal-control", "robotics"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", +] dependencies = ["numpy>=1.22"] +[project.urls] +Homepage = "https://github.com/astomodynamics/cddp-cpp" +Repository = "https://github.com/astomodynamics/cddp-cpp" +Documentation = "https://astomodynamics.github.io/cddp-cpp/" +Issues = "https://github.com/astomodynamics/cddp-cpp/issues" + [project.optional-dependencies] test = ["pytest>=7.0", "matplotlib>=3.5"] viz = ["matplotlib>=3.5"] @@ -21,3 +45,7 @@ dev = ["pytest>=7.0", "matplotlib>=3.5"] [tool.scikit-build] cmake.args = ["-DCDDP_CPP_BUILD_PYTHON=ON", "-DCDDP_CPP_BUILD_TESTS=OFF", "-DCDDP_CPP_BUILD_EXAMPLES=OFF"] wheel.packages = ["python/pycddp"] + +[tool.cibuildwheel] +build-verbosity = 1 +test-command = "python -c \"import pycddp; print(pycddp.__version__)\""