Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/publish_to_pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Publish Python Package

on: push

jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install cibuildwheel
run: |
python -m pip install --upgrade pip
python -m pip install cibuildwheel

- name: Build wheels
env:
CIBW_BUILD: cp${{ matrix.python-version == '3.10' && '310' || matrix.python-version == '3.11' && '311' || matrix.python-version == '3.12' && '312' || matrix.python-version == '3.9' && '39' || '38' }}-manylinux_x86_64
CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
CIBW_BEFORE_BUILD: |
yum install -y eigen3-devel cmake3
pip install -r python/requirements.txt
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
CIBW_REPAIR_WHEEL_COMMAND: auditwheel repair -w {dest_dir} {wheel}
run: |
python -m cibuildwheel python --output-dir wheelhouse

- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions-${{ matrix.python-version }}
path: dist/

publish-to-pypi:
name: Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/qpSWIFT_sparse_bindings
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
pattern: python-package-distributions-*
path: dist/
merge-multiple: true
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://test.pypi.org/legacy/ # remove/comment line when publishing to actual repository
# user: __token__ # used before setting up a trusted publisher
# password: ${{ secrets.PYPI_API_TOKEN }}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Python Bindings for qpSWIFT

These are custom Python bindings for a sparse interface for the [qpSWIFT](https://github.com/qpSWIFT/qpSWIFT) QP solver.

> qpSWIFT is developed and maintained by its original authors [here](https://github.com/qpSWIFT/qpSWIFT) and is licensed under the GNU General Public License v3.0 (GPL-3.0).
> This project provides Python bindings and is independently maintained.

---

## Original qpSWIFT README
# qpSWIFT
![github_release](https://img.shields.io/github/release-date/qpSWIFT/qpSWIFT)
![license](https://img.shields.io/github/license/qpSWIFT/qpSWIFT)
Expand Down
4 changes: 2 additions & 2 deletions python/pyqpSWIFT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,8 +601,8 @@ QPOptions dict_to_options(const py::dict& options_dict) {
static ThreadPool pool(std::max<unsigned>(1u, std::thread::hardware_concurrency() - 1));

// Python module
PYBIND11_MODULE(qpSWIFT, m) {
m.doc() = "Python bindings for qpSWIFT quadratic programming solver";
PYBIND11_MODULE(qpSWIFT_sparse_bindings, m) {
m.doc() = "Custom sparse python bindings for the qpSWIFT solver";

py::class_<QPSolution>(m, "QPSolution")
.def_readonly("exit_flag", &QPSolution::exit_flag, "Exit flag (0: success, >0: error)")
Expand Down
3 changes: 3 additions & 0 deletions python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pybind11
scipy
numpy
2 changes: 1 addition & 1 deletion python/demoqp.py → python/scripts/demoqp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
import qpSWIFT
import qpSWIFT_sparse_bindings as qpSWIFT


### Solver Options
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import numpy as np
import scipy.sparse as sp
import qpSWIFT
import qpSWIFT_sparse_bindings as qpSWIFT

# Solver Options
# For information about Solver options please refer to qpSWIFT documentation
Expand Down
260 changes: 130 additions & 130 deletions python/syntax.py → python/scripts/syntax.py

Large diffs are not rendered by default.

278 changes: 150 additions & 128 deletions python/setup.py
Original file line number Diff line number Diff line change
@@ -1,128 +1,150 @@
import os
import sys
import subprocess
from setuptools import setup, Extension
import pybind11
import numpy as np

# This setup.py builds the QP_SWIFT C sources inline, relative to this python/ folder
here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.abspath(os.path.join(here, os.pardir))

# Paths for includes and sources
include_dir = os.path.join(project_root, 'include')
src_dir = os.path.join(project_root, 'src')

# Verify directories
if not os.path.isdir(include_dir):
sys.exit(f"ERROR: include directory not found: {include_dir}")
if not os.path.isdir(src_dir):
sys.exit(f"ERROR: src directory not found: {src_dir}")


# Function to find Eigen include directory
def find_eigen():
# Common paths where Eigen might be installed
common_paths = [
"/usr/include/eigen3",
"/usr/local/include/eigen3",
"/opt/homebrew/include/eigen3",
"/usr/include",
"/usr/local/include",
"/opt/local/include/eigen3",
"/opt/local/include",
]

# Try to find Eigen/Core header in common paths
for path in common_paths:
if os.path.exists(os.path.join(path, "Eigen", "Core")):
return path

# Try using pkg-config if available
try:
eigen_cflags = subprocess.check_output(["pkg-config", "--cflags", "eigen3"],
universal_newlines=True).strip()
for item in eigen_cflags.split():
if item.startswith("-I"):
path = item[2:]
if os.path.exists(os.path.join(path, "Eigen", "Core")):
return path
except (subprocess.SubprocessError, FileNotFoundError):
pass

# Try using find command if available (Unix-like systems)
try:
find_output = subprocess.check_output(
["find", "/usr", "-name", "Eigen", "-type", "d", "-path", "*/eigen3/Eigen"],
universal_newlines=True
).strip()

if find_output:
# Take the first result and get its parent directory
eigen_dir = os.path.dirname(find_output.split('\n')[0])
if os.path.exists(os.path.join(eigen_dir, "Eigen", "Core")):
return eigen_dir
except (subprocess.SubprocessError, FileNotFoundError):
pass

return None


# Find Eigen include directory
eigen_include_dir = find_eigen()

if eigen_include_dir is None:
print("WARNING: Eigen headers not found automatically. Building might fail.")
print("You may need to install Eigen with: apt install libeigen3-dev (Debian/Ubuntu),")
print("brew install eigen (macOS), or download from http://eigen.tuxfamily.org/")
else:
print(f"Found Eigen headers at: {eigen_include_dir}")

# Collect all C source files
c_sources = [
os.path.join(src_dir, f)
for f in [
'amd_1.c', 'amd_2.c', 'amd_aat.c', 'amd_control.c', 'amd_defaults.c',
'amd_dump.c', 'amd_global.c', 'amd_info.c', 'amd_order.c',
'amd_post_tree.c', 'amd_postorder.c', 'amd_preprocess.c', 'amd_valid.c',
'Auxilary.c', 'ldl.c', 'timer.c', 'qpSWIFT.c'
]
]

# Add the pybind11 wrapper
wrapper_cpp = os.path.join(here, 'pyqpSWIFT.cpp')
c_sources.append(wrapper_cpp)

# Setup include directories
include_dirs = [
include_dir,
pybind11.get_include(),
pybind11.get_include(user=True),
np.get_include(),
]

# Add Eigen include directory if found
if eigen_include_dir:
include_dirs.append(eigen_include_dir)

# Prepare extra compile args
extra_compile_args = ["-O3", "-std=c++11"]

ext_modules = [
Extension(
name="qpSWIFT",
sources=c_sources,
include_dirs=include_dirs,
language="c++",
extra_compile_args=extra_compile_args,
),
]

setup(
name="qpSWIFT",
version="0.1.0",
description="Python bindings for the QP_SWIFT solver",
ext_modules=ext_modules,
install_requires=["numpy", "scipy", "pybind11"],
zip_safe=False,
)
import os
import sys
import subprocess
from setuptools import setup, Extension
import pybind11
import numpy as np

# This setup.py builds the QP_SWIFT C sources inline, relative to this python/ folder
here = os.path.abspath(os.path.dirname(__file__))
project_root = os.path.abspath(os.path.join(here, os.pardir))

# Paths for includes and sources
include_dir = os.path.join(project_root, 'include')
src_dir = os.path.join(project_root, 'src')

# Verify directories
if not os.path.isdir(include_dir):
sys.exit(f"ERROR: include directory not found: {include_dir}")
if not os.path.isdir(src_dir):
sys.exit(f"ERROR: src directory not found: {src_dir}")


# Function to find Eigen include directory
def find_eigen():
# Common paths where Eigen might be installed
common_paths = [
"/usr/include/eigen3",
"/usr/local/include/eigen3",
"/opt/homebrew/include/eigen3",
"/usr/include",
"/usr/local/include",
"/opt/local/include/eigen3",
"/opt/local/include",
]

# Try to find Eigen/Core header in common paths
for path in common_paths:
if os.path.exists(os.path.join(path, "Eigen", "Core")):
return path

# Try using pkg-config if available
try:
eigen_cflags = subprocess.check_output(["pkg-config", "--cflags", "eigen3"],
universal_newlines=True).strip()
for item in eigen_cflags.split():
if item.startswith("-I"):
path = item[2:]
if os.path.exists(os.path.join(path, "Eigen", "Core")):
return path
except (subprocess.SubprocessError, FileNotFoundError):
pass

# Try using find command if available (Unix-like systems)
try:
find_output = subprocess.check_output(
["find", "/usr", "-name", "Eigen", "-type", "d", "-path", "*/eigen3/Eigen"],
universal_newlines=True
).strip()

if find_output:
# Take the first result and get its parent directory
eigen_dir = os.path.dirname(find_output.split('\n')[0])
if os.path.exists(os.path.join(eigen_dir, "Eigen", "Core")):
return eigen_dir
except (subprocess.SubprocessError, FileNotFoundError):
pass

return None


# Find Eigen include directory
eigen_include_dir = find_eigen()

if eigen_include_dir is None:
print("WARNING: Eigen headers not found automatically. Building might fail.")
print("You may need to install Eigen with: apt install libeigen3-dev (Debian/Ubuntu),")
print("brew install eigen (macOS), or download from http://eigen.tuxfamily.org/")
else:
print(f"Found Eigen headers at: {eigen_include_dir}")

# Collect all C source files
c_sources = [
os.path.join(src_dir, f)
for f in [
'amd_1.c', 'amd_2.c', 'amd_aat.c', 'amd_control.c', 'amd_defaults.c',
'amd_dump.c', 'amd_global.c', 'amd_info.c', 'amd_order.c',
'amd_post_tree.c', 'amd_postorder.c', 'amd_preprocess.c', 'amd_valid.c',
'Auxilary.c', 'ldl.c', 'timer.c', 'qpSWIFT.c'
]
]

# Add the pybind11 wrapper
wrapper_cpp = os.path.join(here, 'pyqpSWIFT.cpp')
c_sources.append(wrapper_cpp)

# Setup include directories
include_dirs = [
include_dir,
pybind11.get_include(),
np.get_include(),
]

# Add Eigen include directory if found
if eigen_include_dir:
include_dirs.append(eigen_include_dir)

# Prepare extra compile args
extra_compile_args = ["-O3", "-std=c++11"]

ext_modules = [
Extension(
name="qpSWIFT_sparse_bindings",
sources=c_sources,
include_dirs=include_dirs,
language="c++",
extra_compile_args=extra_compile_args,
),
]

setup(
name="qpSWIFT_sparse_bindings",
version="1.0.0",
description="Custom sparse python bindings for the qpSWIFT solver",
long_description_content_type="text/markdown",
python_requires='>=3.8, <3.13',
long_description='qpSWIFT is light-weight sparse Quadratic Programming solver targeted for embedded and robotic applications. '
'It employs Primal-Dual Interioir Point method with Mehrotra Predictor corrector step and Nesterov Todd scaling. '
'For solving the linear system of equations, sparse LDL\' factorization is used along with approximate minimum '
'degree heuristic to minimize fill-in of the factorizations.\n '
'This is a fork of the official qpSWIFT project (https://github.com/qpSWIFT/qpSWIFT) adapted for use with Giskard(py)',
ext_modules=ext_modules,
url="https://github.com/SemRoCo/qpSWIFT",
author="Simon Stelter",
install_requires=["numpy", "scipy", "pybind11"],
license='GPLv3',
license_files=('LICENSE',),
classifiers=[
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: POSIX :: Linux',
'Intended Audience :: Science/Research',
"Programming Language :: Python",
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
],
zip_safe=False,
)
Loading
Loading