From 6c6c855f0d84add0bf91cd86e1eff361c90b1bcd Mon Sep 17 00:00:00 2001 From: Nathan Baker Date: Sat, 12 Sep 2026 16:44:55 -0700 Subject: [PATCH] Restore Python binding builds --- .github/workflows/python-package.yml | 67 ++++++++++++++++++++++++++++ CMakeLists.txt | 10 +++++ externals/pybind11 | 2 +- tools/python-pybind/CMakeLists.txt | 14 +++++- tools/python-pybind/bind_nosh.cpp | 22 +++------ tools/python/CMakeLists.txt | 23 +++------- tools/python/apbslib.i | 12 ++--- tools/python/main.py | 16 +++---- tools/python/noinput.py | 14 +++--- tools/python/test_bindings.py | 40 +++++++++++++++++ 10 files changed, 162 insertions(+), 58 deletions(-) create mode 100644 tools/python/test_bindings.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 509924a3..3b84bf91 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -56,3 +56,70 @@ jobs: path: | coverage.txt coverage.xml + + bindings: + name: Python bindings + runs-on: ubuntu-22.04 + timeout-minutes: 30 + env: + CMAKE_POLICY_VERSION_MINIMUM: "3.5" + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python 3.14 + uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + bison \ + build-essential \ + flex \ + libboost-dev \ + libeigen3-dev \ + liblapack-dev \ + libopenblas-serial-dev \ + libreadline-dev \ + libsuitesparse-dev \ + swig + git clone --depth 1 https://github.com/Electrostatics/metis-fork.git /tmp/metis + cd /tmp/metis + make config prefix=/usr/local + sudo make install + + - name: Configure Python bindings + run: | + cmake -S . -B build-bindings \ + -DAPBS_STATIC_BUILD=OFF \ + -DBLA_VENDOR=OpenBLAS \ + -DBUILD_DOC=OFF \ + -DBUILD_SHARED_LIBS=ON \ + -DBUILD_TOOLS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$PWD/build-bindings/install" \ + -DENABLE_BEM=OFF \ + -DENABLE_GEOFLOW=OFF \ + -DENABLE_iAPBS=OFF \ + -DENABLE_PBAM=OFF \ + -DENABLE_PBSAM=OFF \ + -DENABLE_PYGBE=OFF \ + -DENABLE_PYTHON=ON \ + -DENABLE_TESTS=OFF \ + -DGET_NanoShaper=OFF \ + -DPYTHON_VERSION=3.14 + + - name: Build and install Python bindings + run: cmake --build build-bindings --parallel 2 --target install + + - name: Test Python bindings + run: | + cd /tmp + PYTHONPATH="$GITHUB_WORKSPACE/build-bindings/install/lib" \ + python "$GITHUB_WORKSPACE/tools/python/test_bindings.py" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ca73194..39db3b0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,16 @@ set(FETK_VERSION "57195e55351e04ce6ee0ef56a143c996a9aee7e2" CACHE STRING "Versio include(ImportFETK) import_fetk(${FETK_VERSION}) +if(NOT APBS_STATIC_BUILD) + find_package(BLAS REQUIRED) + if(TARGET cgcode) + target_link_libraries(cgcode PRIVATE vf2c BLAS::BLAS) + endif() + if(TARGET superlu) + target_link_libraries(superlu PRIVATE BLAS::BLAS) + endif() +endif() + if(APPLE AND CMAKE_C_COMPILER_ID MATCHES "AppleClang|Clang" AND TARGET vf2c) target_compile_options(vf2c PRIVATE -Wno-error=implicit-int) endif() diff --git a/externals/pybind11 b/externals/pybind11 index fbc75636..97bf890d 160000 --- a/externals/pybind11 +++ b/externals/pybind11 @@ -1 +1 @@ -Subproject commit fbc7563623247f410b119f7cee44841411059cd7 +Subproject commit 97bf890db679505a14dfe547a5e77bb2bd05dc90 diff --git a/tools/python-pybind/CMakeLists.txt b/tools/python-pybind/CMakeLists.txt index 5393df9f..60c1ef79 100644 --- a/tools/python-pybind/CMakeLists.txt +++ b/tools/python-pybind/CMakeLists.txt @@ -1,5 +1,17 @@ -find_package(pybind11 REQUIRED) +set(PYBIND11_FINDPYTHON ON) +find_package(pybind11 CONFIG QUIET) +if(NOT TARGET pybind11::module) + if(NOT EXISTS ${CMAKE_SOURCE_DIR}/externals/pybind11/CMakeLists.txt) + message(FATAL_ERROR "pybind11 was not found and its submodule is unavailable") + endif() + add_subdirectory( + ${CMAKE_SOURCE_DIR}/externals/pybind11 + ${CMAKE_BINARY_DIR}/externals/pybind11 + EXCLUDE_FROM_ALL + ) +endif() + add_library(apbs_pybind MODULE module.cpp diff --git a/tools/python-pybind/bind_nosh.cpp b/tools/python-pybind/bind_nosh.cpp index 5944cf7f..5bdecfb9 100644 --- a/tools/python-pybind/bind_nosh.cpp +++ b/tools/python-pybind/bind_nosh.cpp @@ -1,17 +1,18 @@ #include "bind_nosh.hpp" #include "bind_valist.hpp" -int parseInputFromString(NOsh *nosh, std::string str) +int parseInputFromString(NOsh *nosh, const std::string& str) { int ret, bufsize; Vio *sock; startVio(); - VASSERT( bufsize <= VMAX_BUFSIZE ); + VASSERT( str.size() <= VMAX_BUFSIZE ); + bufsize = static_cast(str.size()); sock = Vio_ctor("BUFF","ASC",VNULL,"0","r"); - Vio_bufTake(sock, const_cast(str.c_str()), str.size()); + Vio_bufTake(sock, const_cast(str.c_str()), bufsize); ret = NOsh_parseInput(nosh, sock); sock->VIObuffer = VNULL; @@ -59,20 +60,7 @@ void bind_nosh(py::module& m) .def("parseInputFromString", [] (NOsh& self, std::string str) -> int { - int ret, bufsize; - Vio *sock; - - startVio(); - - VASSERT( bufsize <= VMAX_BUFSIZE ); - sock = Vio_ctor("BUFF","ASC",VNULL,"0","r"); - - Vio_bufTake(sock, const_cast(str.c_str()), str.size()); - - ret = NOsh_parseInput(&self, sock); - sock->VIObuffer = VNULL; - Vio_dtor(&sock); - return ret; + return parseInputFromString(&self, str); }) .def("__del__", [] (NOsh* self) diff --git a/tools/python/CMakeLists.txt b/tools/python/CMakeLists.txt index 85dddea5..ba00bcb8 100644 --- a/tools/python/CMakeLists.txt +++ b/tools/python/CMakeLists.txt @@ -3,7 +3,7 @@ if (${CMAKE_VERSION} VERSION_GREATER "3.13") CMAKE_POLICY(SET CMP0086 OLD) endif() -FIND_PACKAGE(SWIG REQUIRED) +FIND_PACKAGE(SWIG 4.0 REQUIRED) INCLUDE(${SWIG_USE_FILE}) @@ -36,18 +36,9 @@ endif(APPLE) SWIG_LINK_LIBRARIES(apbslib ${APBS_LIBS} ${APBS_INTERNAL_LIBS} ${Python3_LIBRARIES}) -if(WIN32) - add_custom_command(TARGET _apbslib POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy ${CMAKE_CURRENT_BINARY_DIR}/apbslib.py ${CMAKE_SOURCE_DIR}/../pdb2pqr/pdb2pka - COMMAND ${CMAKE_COMMAND} -E - copy ${LIBRARY_OUTPUT_PATH}/$/_apbslib.pyd ${CMAKE_SOURCE_DIR}/../pdb2pqr/pdb2pka - ) -else() - add_custom_command(TARGET _apbslib POST_BUILD - COMMAND ${CMAKE_COMMAND} -E - copy ${CMAKE_CURRENT_BINARY_DIR}/apbslib.py ${CMAKE_SOURCE_DIR}/../pdb2pqr/pdb2pka - COMMAND ${CMAKE_COMMAND} -E - copy ${LIBRARY_OUTPUT_PATH}/_apbslib${CMAKE_SHARED_LIBRARY_SUFFIX} ${CMAKE_SOURCE_DIR}/../pdb2pqr/pdb2pka - ) -endif() +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/apbslib.py + DESTINATION lib + COMPONENT python) +install(TARGETS ${SWIG_MODULE_apbslib_REAL_NAME} + LIBRARY DESTINATION lib COMPONENT python + RUNTIME DESTINATION lib COMPONENT python) diff --git a/tools/python/apbslib.i b/tools/python/apbslib.i index be7d3d56..0b20e6ab 100644 --- a/tools/python/apbslib.i +++ b/tools/python/apbslib.i @@ -272,18 +272,18 @@ void set_entry(double *array, int i, double val){ %inline %{ -int parseInputFromString(NOsh *nosh, PyObject *string){ +int parseInputFromString(NOsh *nosh, const char *string){ int ret, bufsize; Vio *sock; startVio(); - bufsize = PyString_Size(string); + bufsize = strlen(string); VASSERT( bufsize <= VMAX_BUFSIZE ); sock = Vio_ctor("BUFF","ASC",VNULL,"0","r"); - Vio_bufTake(sock, PyString_AsString(string), bufsize); + Vio_bufTake(sock, (char *)string, bufsize); ret = NOsh_parseInput(nosh, sock); sock->VIObuffer = VNULL; @@ -441,9 +441,9 @@ PyObject *getForces(AtomForce **atomForce, Valist *alist){ dbholder = PyList_New(3); ibholder = PyList_New(3); - qf = PyString_FromString("qf"); - db = PyString_FromString("db"); - ib = PyString_FromString("ib"); + qf = PyUnicode_FromString("qf"); + db = PyUnicode_FromString("db"); + ib = PyUnicode_FromString("ib"); for (i=0;i 0: stdout.write("---------------------------------------------\n") stdout.write("PRINT STATEMENTS\n") - for iprint in xrange(nosh.nprint): + for iprint in range(nosh.nprint): if NOsh_printWhat(nosh, iprint) == NPT_ENERGY: printEnergy(com, nosh, totEnergy, iprint) elif NOsh_printWhat(nosh, iprint) == NPT_FORCE: @@ -427,11 +425,11 @@ def main(): elif NOsh_printWhat(nosh, iprint) == NPT_ELECENERGY: printElecEnergy(com, nosh, totEnergy, iprint) elif NOsh_printWhat(nosh, iprint) == NPT_ELECFORCE: - printElecForce(com, nosh, nforce, atomForce, iprint) + printElecForce(com, nosh, nforce, atomforce, iprint) elif NOsh_printWhat(nosh, iprint) == NPT_APOLENERGY: printApolEnergy(nosh, iprint) elif NOsh_printWhat(nosh, iprint) == NPT_APOLFORCE: - printApolForce(com, nosh, nforce, atomForce, iprint) + printApolForce(com, nosh, nforce, atomforce, iprint) else: stdout.write("Undefined PRINT keyword!\n") break @@ -472,7 +470,7 @@ def main(): stdout.write("Thanks for using APBS!\n\n") # Stop the main timer - main_timer_stop = time.clock() + main_timer_stop = time.perf_counter() stdout.write( "Total execution time: %1.6e sec\n" % (main_timer_stop - main_timer_start) diff --git a/tools/python/noinput.py b/tools/python/noinput.py index 56002cf7..e14debaf 100644 --- a/tools/python/noinput.py +++ b/tools/python/noinput.py @@ -94,11 +94,9 @@ wrap_forceMG, writedataMG, writematMG, - xrange, ) import sys import time -import string import re from sys import stdout, stderr @@ -317,7 +315,7 @@ def printResults(energyList, potList, forceList): factor = getUnitConversion() - for i in xrange(len(potList)): + for i in range(len(potList)): list = potList[i] print(f"\nPer-atom potentials from calculation {i}") for j in range(len(list)): @@ -384,7 +382,7 @@ def runAPBS(PQR, INPUT): atomforce = new_atomforcelist(NOSH_MAXCALC) # Start the main timer - main_timer_start = time.clock() + main_timer_start = time.perf_counter() # Parse the input file nosh = NOsh_ctor(rank, size) @@ -400,7 +398,7 @@ def runAPBS(PQR, INPUT): # the need for an actual PQR file from stdin alist = new_valist(NOSH_MAXMOL) - atoms = string.split(PQR, "\n") + atoms = PQR.split("\n") for i in range(len(atoms)): atom = atoms[i] if not (atom.startswith("ATOM") or atom.startswith("HETATM")): @@ -413,7 +411,7 @@ def runAPBS(PQR, INPUT): if re.compile(r"( [A-Z]{3} [A-Z]{1} *\d+)").findall(atom) != []: haschain = 1 - params = string.split(atom) + params = atom.split() x.append(float(params[5 + haschain])) y.append(float(params[6 + haschain])) z.append(float(params[7 + haschain])) @@ -576,7 +574,7 @@ def runAPBS(PQR, INPUT): if nosh.nprint > 0: stdout.write("---------------------------------------------\n") stdout.write("PRINT STATEMENTS\n") - for iprint in xrange(nosh.nprint): + for iprint in range(nosh.nprint): if NOsh_printWhat(nosh, iprint) == NPT_ENERGY: printEnergy(com, nosh, totEnergy, iprint) elif NOsh_printWhat(nosh, iprint) == NPT_FORCE: @@ -621,7 +619,7 @@ def runAPBS(PQR, INPUT): stdout.write("Thanks for using APBS!\n\n") # Stop the main timer - main_timer_stop = time.clock() + main_timer_stop = time.perf_counter() stdout.write( "Total execution time: %1.6e sec\n" % (main_timer_stop - main_timer_start) diff --git a/tools/python/test_bindings.py b/tools/python/test_bindings.py new file mode 100644 index 00000000..87e4b7a4 --- /dev/null +++ b/tools/python/test_bindings.py @@ -0,0 +1,40 @@ +"""Smoke tests for the installed APBS Python extension modules.""" + +import ast +from importlib import import_module +from pathlib import Path + + +def load_example_input(): + """Load the input string without executing the legacy driver.""" + source = Path(__file__).with_name("noinput.py").read_text(encoding="utf-8") + tree = ast.parse(source) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if any( + isinstance(target, ast.Name) and target.id == "INPUT" + for target in node.targets + ): + return ast.literal_eval(node.value) + raise RuntimeError("Could not find INPUT in noinput.py") + + +def main(): + """Parse the same APBS input through both binding implementations.""" + apbs = import_module("apbs") + apbslib = import_module("apbslib") + input_text = load_example_input() + + legacy_nosh = apbslib.NOsh_ctor(0, 1) + try: + assert apbslib.parseInputFromString(legacy_nosh, input_text) == 1 + finally: + apbslib.delete_Nosh(legacy_nosh) + + modern_nosh = apbs.NOsh() + assert modern_nosh.parseInputFromString(input_text) == 1 + + +if __name__ == "__main__": + main()