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
67 changes: 67 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion externals/pybind11
Submodule pybind11 updated 376 files
14 changes: 13 additions & 1 deletion tools/python-pybind/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 5 additions & 17 deletions tools/python-pybind/bind_nosh.cpp
Original file line number Diff line number Diff line change
@@ -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<int>(str.size());
sock = Vio_ctor("BUFF","ASC",VNULL,"0","r");

Vio_bufTake(sock, const_cast<char*>(str.c_str()), str.size());
Vio_bufTake(sock, const_cast<char*>(str.c_str()), bufsize);

ret = NOsh_parseInput(nosh, sock);
sock->VIObuffer = VNULL;
Expand Down Expand Up @@ -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<char*>(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)
Expand Down
23 changes: 7 additions & 16 deletions tools/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})


Expand Down Expand Up @@ -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}/$<CONFIG>/_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)
12 changes: 6 additions & 6 deletions tools/python/apbslib.i
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Valist_getNumberAtoms(alist);i++){
for (j=0;j<3;j++){
Expand Down
16 changes: 7 additions & 9 deletions tools/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
Vcom_rank,
Vcom_size,
Vmem_ctor,
atomForce,
delete_Com,
delete_Mem,
delete_Nosh,
Expand Down Expand Up @@ -126,7 +125,6 @@
wrap_forceMG,
writedataMG,
writematMG,
xrange,
)
import sys
import time
Expand Down Expand Up @@ -271,7 +269,7 @@ def main():
atomforce = new_atomforcelist(NOSH_MAXCALC)

# Start the main timer
main_timer_start = time.clock()
main_timer_start = time.perf_counter()

# Check invocation
stdout.write(getHeader())
Expand Down Expand Up @@ -324,10 +322,10 @@ def main():

stdout.write("Preparing to run %d PBE calculations. \n" % nosh.ncalc)

for icalc in xrange(nosh.ncalc):
for icalc in range(nosh.ncalc):
totEnergy.append(0.0)

for icalc in xrange(nosh.ncalc):
for icalc in range(nosh.ncalc):
stdout.write("---------------------------------------------\n")
calc = NOsh_getCalc(nosh, icalc)
mgparm = calc.mgparm
Expand Down Expand Up @@ -419,19 +417,19 @@ def main():
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:
printForce(com, nosh, nforce, atomforce, iprint)
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
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions tools/python/noinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,9 @@
wrap_forceMG,
writedataMG,
writematMG,
xrange,
)
import sys
import time
import string
import re
from sys import stdout, stderr

Expand Down Expand Up @@ -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)):
Expand Down Expand Up @@ -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)
Expand All @@ -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")):
Expand All @@ -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]))
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions tools/python/test_bindings.py
Original file line number Diff line number Diff line change
@@ -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()
Loading