Skip to content

Commit

Permalink
Added Python bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
nbelakovski committed Apr 21, 2024
1 parent e559913 commit 2f404d4
Show file tree
Hide file tree
Showing 36 changed files with 1,931 additions and 33 deletions.
43 changes: 43 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,7 @@ constrc
execstack
FUNPTR
PROCPOINTER
PYTHONPATH
cfun
cobj
NVHPC
Expand Down Expand Up @@ -2190,3 +2191,45 @@ nosplash
noopengl
ogfile
nend
capfd
cibuildwheel
dtype
dummybaseobject
GIL
maxcv
myargs
ndarray
newfun
NEWPYTHON
nfev
nlconstrlist
nlcs
pybind
pypa
pystr
rtol
scikit
ucrt
whl
xlist
ARCHS
CIBW
cibw
rtools
amd
edgeitems
printoptions
maxfev
testname
skipif
outerr
libprimac
libprimaf
libprimafc
htmlcov
delocate
broadcastable
autoselection
auditwheel
Cbuild
Ceditable
61 changes: 61 additions & 0 deletions .github/workflows/build_python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Build python wheels

on: [push, pull_request]

jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, windows-2019, macos-11]

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Get tags for use with git describe

- name: Checkout pybind11 submodule
run: git submodule update --init python/pybind11

- uses: fortran-lang/setup-fortran@main
if: ${{ runner.os == 'macOS' }}
with:
compiler: gcc
version: 8

# Copied from https://github.com/scipy/scipy/blob/main/.github/workflows/wheels.yml
- name: win_amd64 - install rtools
run: |
# mingw-w64
choco install rtools -y --no-progress --force --version=4.0.0.20220206
echo "c:\rtools40\ucrt64\bin;" >> $env:GITHUB_PATH
if: ${{ runner.os == 'Windows' }}

- name: Build wheels
uses: pypa/cibuildwheel@v2.16.5

- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl

- uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ matrix.os }}-${{ strategy.job-index }}
path: ./prima_htmlcov


build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build sdist
run: pipx run build --sdist

- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: dist/*.tar.gz
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ install/
build/
include/
!c/include/
!python/pybind11/include/
lib/
mod/

Expand Down Expand Up @@ -108,6 +109,7 @@ parts/
sdist/
var/
wheels/
wheelhouse
share/python-wheels/
*.egg-info/
.installed.cfg
Expand Down Expand Up @@ -227,3 +229,6 @@ cython_debug/

# Mac files
.DS_Store

# Version file
_version.txt
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule ".development"]
path = .development
url = git@github.com:libprima/prima_development.git
[submodule "python/pybind11"]
path = python/pybind11
url = https://github.com/pybind/pybind11
49 changes: 40 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,59 @@ endif ()

# Get the version number
find_package(Git)
set(IS_REPO FALSE)
if(GIT_EXECUTABLE)
# --always means git describe will output the commit hash if no tags are found
# This is usually the case for forked repos since they do not clone tags by default.
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always --dirty
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE PRIMA_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE)
else()
# If git is not available, that may indicate we are building on macports which
# downloads the bundle from github (which uses git archive) and so the version
# number should be in .git-archival.txt
file(STRINGS .git-archival.txt PRIMA_VERSION)
if(PRIMA_VERSION MATCHES "describe")
message(WARNING "No git detected and .git-archival.txt does not contain a version number")
set(PRIMA_VERSION "unknown")
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE GIT_RESULT ERROR_QUIET)
if (GIT_RESULT EQUAL 0)
set(IS_REPO TRUE)
endif()
endif()
if(NOT GIT_EXECUTABLE OR NOT IS_REPO)
# If git is not available, or this isn't a repo, that may indicate we are building
# on macports which downloads the bundle from github (which uses git archive) and
# so the version number should be in .git-archival.txt.
# Alternatively it might mean that we're building the Python bindings, in which case
# the version is output in _version.txt. I know, it's complicated. I don't make the rules.
if(EXISTS _version.txt)
file(STRINGS _version.txt PRIMA_VERSION)
else()
file(STRINGS .git-archival.txt PRIMA_VERSION)
if(PRIMA_VERSION MATCHES "describe")
message(WARNING "No git detected and .git-archival.txt does not contain a version number")
set(PRIMA_VERSION "unknown")
endif()
endif()

endif()
# Remove the leading v from PRIMA_VERSION, if it contains one.
string(REGEX REPLACE "^v" "" PRIMA_VERSION ${PRIMA_VERSION})
message(STATUS "Setting PRIMA version to ${PRIMA_VERSION}")

option (PRIMA_ENABLE_PYTHON "Python binding" OFF)
if (PRIMA_ENABLE_PYTHON)
if(NOT PRIMA_ENABLE_C)
message(FATAL_ERROR "Building Python bindings requires C bindings. Please turn on PRIMA_ENABLE_C")
endif()
if(BUILD_SHARED_LIBS)
# This will include libprimaf, libprimafc, and libprimac into the compiled Python binding, removing the need
# to properly set the rpath or find those libraries at runtime.
# Even if we did make it successfully build with shared libraries, delocate/auditwheel will copy them into the
# bindings anyway
message(FATAL_ERROR "Building Python bindings requires static libraries. Please disable BUILD_SHARED_LIBS")
endif()
if(NOT CMAKE_Fortran_COMPILER_ID MATCHES "GNU")
message(WARNING "Compiling Python bindings with compilers other than GNU has not been tested and no support is planned at this time")
endif()
enable_language(CXX)
add_subdirectory(python)
endif ()

install(
TARGETS primaf ${primac_target}
EXPORT prima-targets
Expand Down
7 changes: 6 additions & 1 deletion c/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ set_target_properties(primac PROPERTIES POSITION_INDEPENDENT_CODE ON C_STANDARD

if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(primac PUBLIC PRIMAC_STATIC)
target_link_libraries (primac INTERFACE ${CMAKE_Fortran_IMPLICIT_LINK_LIBRARIES})
# The line below caused issues when compiling prima_pybind on Windows with MinGW. We get errors
# about multiple definition of unwind_resume due to the inclusion of gcc_s.
# It's unclear why this was added as initial reason given in the issue (108) seems to work fine for me
if (NOT WIN32)
target_link_libraries (primac INTERFACE ${CMAKE_Fortran_IMPLICIT_LINK_LIBRARIES})
endif()
endif ()

# Export symbols on Windows. See more comments in fortran/CMakeLists.txt.
Expand Down
12 changes: 7 additions & 5 deletions c/include/prima/prima.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ typedef enum {
PRIMA_NULL_X0 = 112,
PRIMA_NULL_RESULT = 113,
PRIMA_NULL_FUNCTION = 114,
PRIMA_PROBLEM_SOLVER_MISMATCH_NONLINEAR_CONSTRAINTS = 115,
PRIMA_PROBLEM_SOLVER_MISMATCH_LINEAR_CONSTRAINTS = 116,
PRIMA_PROBLEM_SOLVER_MISMATCH_BOUNDS = 117,
PRIMA_RESULT_INITIALIZED = 115,
} prima_rc_t;


Expand Down Expand Up @@ -234,7 +232,7 @@ typedef struct {
// of sqrt(machine epsilon) will be used.
double ctol;

// data: user data, will be passed through the objective function callback
// data: user data, will be passed through the objective function
// Default: NULL
void *data;

Expand Down Expand Up @@ -272,7 +270,7 @@ typedef struct {
int nf;

// status: return code
int status;
prima_rc_t status;

// message: exit message
const char *message;
Expand Down Expand Up @@ -300,6 +298,10 @@ PRIMAC_API
prima_rc_t prima_minimize(const prima_algorithm_t algorithm, const prima_problem_t problem, const prima_options_t options, prima_result_t *const result);


// Function to check if PRIMA returned normally or ran into abnormal conditions
PRIMAC_API
bool prima_is_success(const prima_result_t result);

#ifdef __cplusplus
}
#endif
Expand Down
27 changes: 9 additions & 18 deletions c/prima.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


#include "prima/prima.h"
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
Expand Down Expand Up @@ -65,16 +66,6 @@ prima_rc_t prima_init_options(prima_options_t *const options)
// Function to check whether the problem matches the algorithm
prima_rc_t prima_check_problem(const prima_problem_t problem, const prima_algorithm_t algorithm)
{
if (algorithm != PRIMA_COBYLA && (problem.calcfc || problem.nlconstr0 || problem.m_nlcon > 0))
return PRIMA_PROBLEM_SOLVER_MISMATCH_NONLINEAR_CONSTRAINTS;

if ((algorithm != PRIMA_COBYLA && algorithm != PRIMA_LINCOA) &&
(problem.m_ineq > 0 || problem.m_eq > 0 || problem.Aineq || problem.bineq || problem.Aeq || problem.beq))
return PRIMA_PROBLEM_SOLVER_MISMATCH_LINEAR_CONSTRAINTS;

if ((algorithm != PRIMA_COBYLA && algorithm != PRIMA_LINCOA && algorithm != PRIMA_BOBYQA) && (problem.xl || problem.xu))
return PRIMA_PROBLEM_SOLVER_MISMATCH_BOUNDS;

if (!problem.x0)
return PRIMA_NULL_X0;

Expand All @@ -100,10 +91,10 @@ prima_rc_t prima_init_result(prima_result_t *const result, const prima_problem_t
result->cstrv = NAN;

// nf: number of function evaluations
result->nf = INT_MIN;
result->nf = 0;

// status: return code
result->status = INT_MIN;
result->status = PRIMA_RESULT_INITIALIZED;

// message: exit message
result->message = NULL;
Expand Down Expand Up @@ -192,12 +183,6 @@ const char *prima_get_rc_string(const prima_rc_t rc)
return "NULL result";
case PRIMA_NULL_FUNCTION:
return "NULL function";
case PRIMA_PROBLEM_SOLVER_MISMATCH_NONLINEAR_CONSTRAINTS:
return "Nonlinear constraints were provided for an algorithm that cannot handle them";
case PRIMA_PROBLEM_SOLVER_MISMATCH_LINEAR_CONSTRAINTS:
return "Linear constraints were provided for an algorithm that cannot handle them";
case PRIMA_PROBLEM_SOLVER_MISMATCH_BOUNDS:
return "Bounds were provided for an algorithm that cannot handle them";
default:
return "Invalid return code";
}
Expand Down Expand Up @@ -283,3 +268,9 @@ prima_rc_t prima_minimize(const prima_algorithm_t algorithm, const prima_problem

return info;
}

bool prima_is_success(const prima_result_t result)
{
return (result.status == PRIMA_SMALL_TR_RADIUS ||
result.status == PRIMA_FTARGET_ACHIEVED) && (result.cstrv <= sqrt(DBL_EPSILON));
}
48 changes: 48 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[build-system]
# scikit-build-core claims there's no need to explicitly specify Ninja,
# as it will "automatically be downloaded if needed", but I don't know
# how it determines "if needed", all I know is that we need it, particularly
# for Windows.
requires = ["scikit-build-core", "numpy", "ninja"]
build-backend = "scikit_build_core.build"

[project]
name = "prima"
dependencies = ["numpy"]
dynamic = ["version"]
requires-python = ">= 3.7" # Driving factor is availavility of scikit-build-core

[tool.scikit-build]
cmake.args = ["-G Ninja", "-DBUILD_SHARED_LIBS=OFF", "-DPRIMA_ENABLE_PYTHON=ON"]
cmake.verbose = true
logging.level = "INFO"
metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
sdist.include = [".git-archival.txt"]
install.components = ["Prima_Python_C_Extension"]

[tool.setuptools_scm] # Section required
write_to = "_version.txt"

[tool.cibuildwheel]
build-verbosity = 3
test-command = "coverage run --branch --source=prima,{project} -m pytest -s {project}/python/tests && coverage html -d {project}/prima_htmlcov"
test-requires = ["pytest", "coverage", "packaging"]
# We need scipy and pdfo (which depends on scipy) for compatibility tests.
# scipy is not available on all platforms we support, so we try to install it
# if posssible, otherwise we skip it. The test will skip itself if it cannot
# import scipy. "--only-binary" ensure we do not try to build scipy from
# source (which requires special setup we have no intention of doing).
before-test = "pip install --only-binary :all: scipy pdfo || true"
skip = [
# On windows we get a complaint from CMake:
# "CMake Error at python/pybind11/tools/FindPythonLibsNew.cmake:191 (message):
# Python config failure: Python is 32-bit, chosen compiler is 64-bit"
# I do not see a way to install a 32-bit compiler with the setup-fortran action,
# so we will just build 64-bit wheels on windows.
"*-win32",
# Disable building PyPy wheels on all platforms. If there is interest in supporting PyPy
# we can look into it.
"pp*",
# Disable musllinux for the moment. It successfully built but there was an error when testing
"*musllinux*",
]
Loading

0 comments on commit 2f404d4

Please sign in to comment.