diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6820e69..ae0f8ff6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,9 +29,7 @@ jobs: # fail fast on hangs instead of the 6h default timeout-minutes: 30 env: - CMAKE_C_COMPILER_LAUNCHER: ccache - CMAKE_CXX_COMPILER_LAUNCHER: ccache - # Release builds use -march=native; hash the resolved target so objects + # meson uses ccache when it is on PATH. Release builds use -march=native; hash the resolved target so objects # cached on one runner CPU are never reused on a different CPU CCACHE_COMPILERCHECK: "%compiler% -v;%compiler% -march=native -Q --help=target" defaults: @@ -48,135 +46,86 @@ jobs: key: ${{ matrix.os }}-${{ matrix.part }} max-size: 1G - # the C++ tests need no Python: conda only for the python part - - name: Set up Conda - if: matrix.part == 'python' + # SUNDIALS, Eigen and LAPACK come from conda-forge for both parts + - name: Set up conda uses: conda-incubator/setup-miniconda@v3 with: - auto-update-conda: true - auto-activate-base: false - + miniforge-version: latest + activate-environment: "" + - name: Create conda environment - if: matrix.part == 'python' shell: bash -l {0} run: | - conda create -n pele-env python=${{ matrix.python-version }} -y - conda activate pele-env - python --version + conda create -y -n pele-env -c conda-forge python=${{ matrix.python-version }} \ + sundials eigen blas-devel meson ninja pkg-config - name: Install system dependencies (Linux) if: matrix.os == 'ubuntu-24.04' run: | sudo apt-get update - sudo apt-get install -y gcc g++ gfortran libblas-dev liblapack-dev liblapacke-dev cmake libsuitesparse-dev lcov + sudo apt-get install -y gcc g++ gfortran libsuitesparse-dev lcov - name: Install system dependencies (macOS) if: matrix.os == 'macos-latest' run: | brew list gcc@15 || brew install gcc@15 - brew list cmake || brew install cmake - brew list openblas || brew install openblas - brew list gettext || brew install gettext brew list suite-sparse || brew install suite-sparse + # homebrew gcc for C, C++ and Fortran (Apple clang has no OpenMP/Fortran) + echo "CC=gcc-15" >> $GITHUB_ENV + echo "CXX=g++-15" >> $GITHUB_ENV + echo "FC=gfortran-15" >> $GITHUB_ENV - name: Install Python dependencies if: matrix.part == 'python' shell: bash -l {0} run: | conda activate pele-env - python -m pip install --upgrade pip wheel - # Use modern setuptools and avoid any legacy dependencies - pip install "setuptools>=65" "build>=0.8" - # Install numpy with modern setuptools support + python -m pip install --upgrade pip + pip install meson-python pip install "numpy>=1.24" "scipy>=1.9" pip install matplotlib networkx "cython>=3.0" pip install "sqlalchemy>=1.4,<2.0" munkres scikit-sparse - pip install pytest pyyaml packaging meson ninja + pip install pytest pyyaml packaging # Install coverage tools pip install pytest-cov coverage - - name: Debug Python Configuration - if: matrix.part == 'python' + - name: Get GoogleTest + if: matrix.part == 'cpp' + run: git submodule update --init cpp_tests/gtest + + - name: Build C++ tests + if: matrix.part == 'cpp' shell: bash -l {0} run: | conda activate pele-env - echo "=== Python Configuration Debug ===" - python debug_python_config.py - echo "" - echo "=== Conda Environment ===" - conda info --envs - which python - echo "" - echo "=== Linking Test ===" - echo 'int main(){return 0;}' > test.c - gcc test.c -lpython${{ matrix.python-version }} -v 2>&1 || echo "Direct linking failed" - rm -f test.c a.out - - - name: Get submodules - run: git submodule update --init --recursive - - - name: Install sundials (Linux) - if: matrix.os == 'ubuntu-24.04' - run: | - cd extern - chmod +x sun_inst.sh - ./sun_inst.sh Debug - cd .. - - - name: Install sundials (macOS) - if: matrix.os == 'macos-latest' - run: | - cd extern - chmod +x sun_inst.sh - MACOSX_DEPLOYMENT_TARGET=12.0 CC=gcc-15 CXX=g++-15 ./sun_inst.sh Debug - cd .. + # coverage on Linux; the macOS runner's gcc has no working gcov setup here + coverage=${{ matrix.os == 'ubuntu-24.04' && 'true' || 'false' }} + meson setup build-tests -Dpython=disabled -Dtests=true -Dbuildtype=debug -Db_coverage=$coverage + meson compile -C build-tests - - name: Install Eigen - run: | - cd extern - cp -r eigen/Eigen install/include - - - name: Build C++ tests with coverage (Linux) - if: matrix.os == 'ubuntu-24.04' && matrix.part == 'cpp' - run: | - cd cpp_tests/source - cmake -DCMAKE_BUILD_TYPE=Coverage -DCMAKE_CXX_FLAGS_COVERAGE="-g -O0 --coverage -fprofile-arcs -ftest-coverage" -DCMAKE_C_FLAGS_COVERAGE="-g -O0 --coverage -fprofile-arcs -ftest-coverage" . - make -j4 - - - name: Build C++ tests (macOS) - if: matrix.os == 'macos-latest' && matrix.part == 'cpp' - run: | - cd cpp_tests/source - cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc-15 -DCMAKE_CXX_COMPILER=g++-15 -DCMAKE_PREFIX_PATH=$(brew --prefix openblas) . - make -j4 - - - name: Run C++ tests (macOS) - if: matrix.os == 'macos-latest' && matrix.part == 'cpp' + - name: Run C++ tests + if: matrix.part == 'cpp' + shell: bash -l {0} run: | - cd cpp_tests/source - ./test_main + conda activate pele-env + meson test -C build-tests --print-errorlogs - - name: Run C++ tests with coverage (Linux) + - name: C++ coverage report (Linux) if: matrix.os == 'ubuntu-24.04' && matrix.part == 'cpp' run: | - cd cpp_tests/source - # The coverage target runs test_main (failing on test failure) and filters the report - make pele_tests_coverage + lcov --directory build-tests --capture --output-file coverage.info \ + --ignore-errors usage,inconsistent,empty,count,mismatch --rc geninfo_unexecuted_blocks=1 + lcov --remove coverage.info "/usr/*" "$CONDA/*" "*/cpp_tests/*" "*/Eigen/*" "*gtest*" "*gmock*" \ + --output-file coverage.info.cleaned --ignore-errors unused,empty,count,mismatch lcov --list coverage.info.cleaned - - name: Compile python library (Linux) - if: matrix.os == 'ubuntu-24.04' && matrix.part == 'python' - shell: bash -l {0} - run: | - conda activate pele-env - python setup_with_cmake.py build_ext -i - - - name: Compile python library (macOS) - if: matrix.os == 'macos-latest' && matrix.part == 'python' + - name: Build python library + if: matrix.part == 'python' shell: bash -l {0} run: | conda activate pele-env - MACOSX_DEPLOYMENT_TARGET=12.0 python setup_with_cmake.py build_ext -i + # editable: pytest and coverage run on the source tree + MACOSX_DEPLOYMENT_TARGET=12.0 pip install --no-build-isolation --no-deps -e . -v - name: Run python tests with coverage if: matrix.part == 'python' @@ -191,7 +140,7 @@ jobs: if: matrix.os == 'ubuntu-24.04' with: token: ${{ secrets.CODECOV_TOKEN }} - files: ${{ matrix.part == 'cpp' && './cpp_tests/source/coverage.info.cleaned' || './coverage.xml' }} + files: ${{ matrix.part == 'cpp' && './coverage.info.cleaned' || './coverage.xml' }} flags: ${{ matrix.part }} name: codecov-umbrella fail_ci_if_error: false @@ -205,12 +154,11 @@ jobs: fail-fast: false matrix: # only the newest Python (unpinned, so new releases are picked up automatically); - # Ubuntu uses the system compilers, macOS (no gfortran) conda's - include: - - {os: ubuntu-24.04, python: latest, compilers: ""} - - {os: macos-latest, python: latest, compilers: compilers} + # Ubuntu uses the system gcc, macOS the runner's homebrew gcc + os: [ubuntu-24.04, macos-latest] + python: [latest] runs-on: ${{ matrix.os }} - name: pip-install (${{ matrix.os }}, python ${{ matrix.python }}${{ matrix.compilers && ', conda compilers' || '' }}) + name: pip-install (${{ matrix.os }}, python ${{ matrix.python }}) defaults: run: shell: bash -l {0} @@ -227,13 +175,15 @@ jobs: - name: Create environment run: | if [ "${{ matrix.python }}" = latest ]; then pin=""; else pin="=${{ matrix.python }}"; fi - conda create -y -n pele -c conda-forge "python$pin" sundials eigen blas-devel ${{ matrix.compilers }} + conda create -y -n pele -c conda-forge "python$pin" sundials eigen blas-devel - name: pip install run: | conda activate pele python --version - PELE_NATIVE=0 python -m pip install . -v + # Apple clang has no OpenMP/Fortran + if [ "$RUNNER_OS" = macOS ]; then export CC=gcc-15 CXX=g++-15 FC=gfortran-15; fi + python -m pip install . -v -Csetup-args=-Dnative=false - name: Test installed package run: | diff --git a/.gitmodules b/.gitmodules index 9035061f..6ba7e1dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "extern/sundials"] - path = extern/sundials - url = https://github.com/LLNL/sundials.git [submodule "cpp_tests/gtest"] path = cpp_tests/gtest url = https://github.com/google/googletest.git -[submodule "extern/eigen"] - path = extern/eigen - url = https://gitlab.com/libeigen/eigen.git diff --git a/CMakeLists.txt.in b/CMakeLists.txt.in deleted file mode 100644 index f77685c7..00000000 --- a/CMakeLists.txt.in +++ /dev/null @@ -1,234 +0,0 @@ -# this file is meant to be parsed by python before being a proper CMakeLists.txt file -# in particular, the following strings will be set to their proper values by python when -# making CMakeLists.txt - -cmake_minimum_required(VERSION 3.18) - -project(pele) -message("${CMAKE_C_COMPILER}") -message("${CMAKE_CXX_COMPILER}") -set(numpy_include __NUMPY_INCLUDE__) -set(PYTHON_INCLUDE_DIRS __PYTHON_INCLUDE__) -set(PYTHON_LDFLAGS "__PYTHON_LDFLAGS__") -set(INCLUDE_SUNDIALS __INCLUDE_SUNDIALS__) - -message(PYTHON_LDFLAGS) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -# osx makes the suffix for shared object libraries .dylib -IF(APPLE) - SET(CMAKE_SHARED_LIBRARY_SUFFIX ".so") -ENDIF(APPLE) - -set(EXT_INSTALL_PATH "${CMAKE_SOURCE_DIR}/extern/install") -# a source checkout's extern/install (sun_inst.sh) is preferred over sundials/eigen -# found in the environment (setup.py adds the active conda env) -list(PREPEND CMAKE_PREFIX_PATH ${EXT_INSTALL_PATH}) - -find_package(Eigen3 CONFIG QUIET) -if(TARGET Eigen3::Eigen) - get_target_property(EIGEN3_INCLUDE_DIR Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) -else() - set(EIGEN3_INCLUDE_DIR ${EXT_INSTALL_PATH}/include) -endif() -message("eigen include dir: ${EIGEN3_INCLUDE_DIR}") - -if(INCLUDE_SUNDIALS) - # plain find_library rather than SUNDIALS' exported targets: those can carry - # absolute paths of the toolchain sundials was built with - find_path(SUNDIALS_INCLUDE_DIR cvode/cvode.h REQUIRED) - find_library(SUNDIALS_CVODE_LIB sundials_cvode REQUIRED) - find_library(SUNDIALS_NVEC_LIB sundials_nvecserial REQUIRED) - find_library(SUNDIALS_CORE_LIB sundials_core) # split out in sundials 7 - set(SUNDIALS_LIBRARIES ${SUNDIALS_CVODE_LIB} ${SUNDIALS_NVEC_LIB}) - if(SUNDIALS_CORE_LIB) - list(APPEND SUNDIALS_LIBRARIES ${SUNDIALS_CORE_LIB}) - endif() - message("SUNDIALS: ${SUNDIALS_INCLUDE_DIR} ${SUNDIALS_LIBRARIES}") -endif() - -# #################################################################################### -message("python include dirs: ${PYTHON_INCLUDE_DIRS}") -message("python ldflags: ${PYTHON_LDFLAGS}") -message("numpy include dirs: ${numpy_include}") -include_directories(${numpy_include}) -include_directories(${PYTHON_INCLUDE_DIRS}) - -# note: We set CMAKE_SHARED_LINKER_FLAGS because I couldn't get it to work using -# target_link_librarires(). CMake kept interpreting the flag CoreFoundation -# as a library and appending -l to it and the compiler would complain -# On Linux extensions must not link libpython (symbols resolve at import, and -# conda-forge's python is static). macOS needs the flags for libintl. -unset(CMAKE_SHARED_LINKER_FLAGS CACHE) -if(APPLE) - set(CMAKE_SHARED_LINKER_FLAGS ${PYTHON_LDFLAGS} CACHE STRING flags) -endif() - -# LAPACK (this also searches for BLAS automatically) -find_package(LAPACK REQUIRED) - -# PYTHON_LDFLAGS attempts to link against the gettext libintl library -# The gettext functions are not part of the C standard library on Apple -# See https://cmake.org/cmake/help/latest/module/FindIntl.html -if(APPLE) - find_package(Intl REQUIRED) - include_directories(${Intl_INCLUDE_DIRS}) - get_filename_component(Intl_LIBRARY_DIR ${Intl_LIBRARIES} DIRECTORY) -endif(APPLE) - -# message(${BLAS_LIBRARIES}) - -# lapacke.h comes with Eigen -set(LAPACKE_INCLUDE_DIR ${EIGEN3_INCLUDE_DIR}/Eigen/src/misc) - -enable_language(CXX) -enable_language(Fortran) -# PYTHON_EXECUTABLE is passed in by setup.py - -SET(CMAKE_CXX_FLAGS __COMPILER_EXTRA_ARGS__) - -# cmake_policy(SET CMP0015 NEW) - -# set the pele include directory -set(pele_include ${CMAKE_CURRENT_SOURCE_DIR}/source) -message("pele include directory: ${pele_include}") - -# build the pele library -# NOTE: GLOBBING MIGHT BE A BAD IDEA WHEN ADDING FILES https://stackoverflow.com/questions/1027247/is-it-better-to-specify-source-files-with-glob-or-each-file-individually-in-cmake -# This is a likely source of undefined symbol errors -FILE(GLOB pele_sources ${pele_include}/*.c*) - -set(pele_sources - ${pele_include}/aatopology.cpp - ${pele_include}/atlj.cpp - ${pele_include}/backtracking.cpp - ${pele_include}/bracketing.cpp - ${pele_include}/lbfgs.cpp - ${pele_include}/linesearch.cpp - ${pele_include}/modified_fire.cpp - ${pele_include}/more_thuente.cpp - ${pele_include}/nwpele.cpp - ${pele_include}/pressure_tensor.cpp - ${pele_include}/rotations.cpp - ${pele_include}/xsum.c - ${pele_include}/pbinary.c -) - -set(cvode_pele_source - ${pele_include}/cvode.cpp - ${pele_include}/extended_mixed_descent.cpp - ${pele_include}/mxd_end_only.cpp - ${pele_include}/newton.cpp - ${pele_include}/newton_with_extension.cpp - ${pele_include}/generic_mixed_descent.cpp -) - -if(INCLUDE_SUNDIALS) - set(pele_sources ${pele_sources} ${cvode_pele_source}) -endif() - -include_directories(BEFORE SYSTEM ${pele_include}) -include_directories(BEFORE SYSTEM ${EIGEN3_INCLUDE_DIR}) - -if(INCLUDE_SUNDIALS) - include_directories(BEFORE SYSTEM ${SUNDIALS_INCLUDE_DIR}) - include_directories(BEFORE SYSTEM ${LAPACKE_INCLUDE_DIR}) - message("SUNDIALS LIBRARIES " ${SUNDIALS_LIBRARIES}) -endif() - -# static so the installed extensions don't depend on a libpele_lib.so in the build dir -add_library(pele_lib STATIC ${pele_sources}) -set_target_properties(pele_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) -target_include_directories(pele_lib PUBLIC - ${PYTHON_INCLUDE_DIRS} - ${numpy_include} -) - -if(INCLUDE_SUNDIALS) - target_link_libraries(pele_lib PUBLIC ${SUNDIALS_LIBRARIES}) -endif() -target_link_libraries(pele_lib PUBLIC ${LAPACK_LIBRARIES}) - -# Apple-specific linking -if(APPLE) - target_link_directories(pele_lib PUBLIC ${Intl_LIBRARY_DIR}) -endif(APPLE) - -# Apple changed the ld linker in Xcode 15 which breaks pele. -# The classic linker can be chosen with -ld_classic flag. -# However, on older Xcode versions, this flag throws an error. -# Thus, we simply check here whether this flag exists. -# See https://www.scivision.dev/xcode-ld_classic/ -# For the undefined flag, see -# https://github.com/nasa/cFE/issues/476#issuecomment-579436106 -if(APPLE) - include(CheckLinkerFlag) - check_linker_flag(C "-ld_classic" CLASSIC_FLAG) - if(CLASSIC_FLAG) - set(APPLE_LINK_OPTIONS "LINKER:-undefined,dynamic_lookup,-ld_classic") - else(CLASSIC_FLAG) - set(APPLE_LINK_OPTIONS "LINKER:-undefined,dynamic_lookup") - endif(CLASSIC_FLAG) -endif(APPLE) - -function(make_cython_lib cython_cxx_source) - get_filename_component(library_name ${cython_cxx_source} NAME) - string(REGEX REPLACE "\\.c(xx)?$" "" library_name ${library_name}) - add_library(${library_name} SHARED ${cython_cxx_source}) - - if(APPLE) - target_link_options(${library_name} PUBLIC ${APPLE_LINK_OPTIONS}) - elseif(UNIX) - # For static Python builds, Cython extensions will have undefined - # symbols. We tell the linker to ignore these, as they will be - # resolved at runtime when the module is imported. - # This is the equivalent of -undefined dynamic_lookup on macOS. - target_link_options(${library_name} PUBLIC "LINKER:--unresolved-symbols=ignore-all") - endif() - - target_link_libraries(${library_name} PUBLIC pele_lib) - - set_target_properties(${library_name} PROPERTIES PREFIX "") - set_target_properties(${library_name} PROPERTIES SUFFIX ".so") - message("making library ${library_name} from source ${cython_cxx_source}") -endfunction(make_cython_lib) - -function(make_fortran_lib fortran_source) - get_filename_component(library_name ${fortran_source} NAME) - string(REGEX REPLACE "\\.f90$" "" library_name ${library_name}) - - # a bit of a hack to get the module name - string(REPLACE "/" "." module_name ${fortran_source}) - string(REPLACE ".f90" "" module_name ${module_name}) - #remove the first part of the module name - string(REGEX REPLACE "^[^.]*." "" module_name ${module_name}) - - - set(output_lib ${CMAKE_BINARY_DIR}/${library_name}.so) - - add_custom_command( - OUTPUT ${output_lib} - # meson backend: on Python < 3.12 f2py otherwise defaults to numpy.distutils, - # which is broken with current setuptools - COMMAND ${PYTHON_EXECUTABLE} -m numpy.f2py -c ${fortran_source} -m ${library_name} --backend meson --quiet - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS ${fortran_source} - COMMENT "Building Fortran module ${library_name} with f2py" - ) - - add_custom_target( - ${library_name}_target ALL - DEPENDS ${output_lib} - ) - - # This is a dummy library to make setuptools happy - add_library(${library_name} SHARED IMPORTED) - set_property(TARGET ${library_name} PROPERTY IMPORTED_LOCATION ${output_lib}) - set_property(TARGET ${library_name} APPEND PROPERTY IMPORTED_CONFIGURATIONS "NONE") - set_property(TARGET ${library_name} PROPERTY IMPORTED_LOCATION_NONE ${output_lib}) - - - set_target_properties(${library_name} PROPERTIES PREFIX "") - set_target_properties(${library_name} PROPERTIES SUFFIX ".so") - message("making fortran library ${library_name} from source ${fortran_source}") -endfunction(make_fortran_lib) diff --git a/README.rst b/README.rst index 1572ff9e..8bb45c70 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,11 @@ We recommend creating a conda environment to work with the package Python 3.11 or newer is required. CI tests the latest Python release (currently 3.14) on Linux and macOS. If the machine already has gcc, g++ and gfortran (e.g. :code:`sudo apt install gcc g++ gfortran`), -leave out :code:`compilers` for a much smaller environment. +leave out :code:`compilers` for a much smaller environment. On macOS use homebrew's +gcc (Apple clang has no OpenMP or Fortran):: + + $ brew install gcc openblas + $ CC=gcc-15 CXX=g++-15 FC=gfortran-15 pip install git+https://github.com/martiniani-lab/pele Optional: :code:`scikit-sparse` (sparse Cholesky for rate calculations) and :code:`pymol-open-source` (viewing structures). The GUI (:code:`pele.gui`) still uses @@ -74,27 +78,33 @@ PyQt4, which is not available for current Python versions. Development ----------- -From a clone, in the same environment:: +pele is built with `meson `_ through +`meson-python `_. From a clone, in the same environment:: - $ pip install . # install, or - $ python setup.py build_ext -i # build in place; then put the clone on PYTHONPATH + $ pip install meson-python meson ninja cython numpy + $ pip install --no-build-isolation -e . # editable -Build options are environment variables (or flags to :code:`setup.py`, e.g. :code:`-j 8`): -:code:`PELE_BUILD_TYPE=Debug`, :code:`PELE_WITH_CVODE=0` (no CVODE / attractor -identification; some tests will fail), :code:`PELE_JOBS=8`, and :code:`PELE_NATIVE=0` -(no :code:`-march=native`, for binaries that run on other machines). +Build options are meson options, passed with +:code:`-Csetup-args=...`, e.g. :code:`pip install . -Csetup-args=-Dbuildtype=debug`: -SUNDIALS must be built in double precision (the build checks this). Instead of conda's -SUNDIALS and Eigen you can build them from the submodules; :code:`extern/install` is then -preferred over the environment:: +- :code:`-Dbuildtype=debug` (default :code:`release`) +- :code:`-Dcvode=disabled`: no CVODE / attractor identification; some tests will fail +- :code:`-Dnative=false`: no :code:`-march=native`, for binaries that run on other machines - $ git submodule update --init --recursive - $ cd extern && ./sun_inst.sh Release && cp -r eigen/Eigen install/include/ && cd .. +The editable install keeps its build in :code:`build/`; pass :code:`-Cbuild-dir=...` to choose +another directory. Editable means code edits will lead to fresh rebuild for C++ code the next time +you `import pele`. python edits will automatically reflect. + +SUNDIALS, Eigen and LAPACK are taken from the active conda environment (or, without one, +from the system, e.g. :code:`sudo apt install libsundials-dev libeigen3-dev liblapack-dev liblapacke-dev`). +pele links LAPACK itself; it needs the LAPACKE package only for the header :code:`lapack.h` +(conda's :code:`blas-devel` provides both). +SUNDIALS must be built in double precision. A :code:`CPATH`/:code:`PYTHONPATH` pointing at a pele clone takes precedence over the -installed package. If a build fails, remove cached files before trying again:: +installed package. If a build fails, remove the build directory before trying again:: - $ rm -rf build cythonize.dat CMakeLists.txt + $ rm -rf build Tests ===== @@ -102,22 +112,22 @@ Tests The project uses GitHub Actions for continuous integration (CI) testing on both Linux and macOS. The badges at the top of this README show the current build status and code coverage. -The C++ tests use GoogleTest. To run the tests, after running `git submodule update --init --recursive` to get the GoogleTest submodule if you haven't already, run:: +The C++ tests use GoogleTest (the :code:`cpp_tests/gtest` submodule, or a system GoogleTest) +and need no Python packages:: - $ cd cpp_tests/source - $ cmake -DCMAKE_BUILD_TYPE=Debug . - $ make -j8 - $ ./test_main - -On MacOs, use the same commands but make sure that cmake finds -the correct GNU compilers and the OpenBLAS library:: + $ git submodule update --init --recursive + $ meson setup build-tests -Dpython=disabled -Dtests=true -Dbuildtype=debug + $ meson test -C build-tests - $ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc-13 -DCMAKE_CXX_COMPILER=g++-13 -DCMAKE_PREFIX_PATH=$(brew --prefix openblas) . +Add :code:`-Db_sanitize=address` for AddressSanitizer or :code:`-Db_coverage=true` for +coverage. On macOS, prefix :code:`meson setup` with :code:`CC=gcc-15 CXX=g++-15`. The +benchmarks in :code:`cpp_tests/source/benchmarks` build on request, e.g. +:code:`ninja -C build-tests cpp_tests/bench_lj`. To run the Python tests on an installed pele:: $ pip install pytest $ OMP_NUM_THREADS=1 pytest --pyargs pele -or :code:`pytest pele/` from a clone with an in-place build. For coverage reporting (as in CI), +or :code:`pytest pele/` from a clone with an editable install. For coverage reporting (as in CI), add :code:`--cov=pele --cov-report=term-missing`. diff --git a/conda-recipe/recipe.yaml b/conda-recipe/recipe.yaml index 029c6484..c4f5c449 100644 --- a/conda-recipe/recipe.yaml +++ b/conda-recipe/recipe.yaml @@ -18,9 +18,8 @@ build: skip: - win script: - env: - PELE_NATIVE: "0" # portable binaries, no -march=native - content: ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation + # portable binaries, no -march=native + content: ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation -Csetup-args=-Dnative=false requirements: build: @@ -28,9 +27,10 @@ requirements: - ${{ compiler('cxx') }} - ${{ compiler('fortran') }} - ${{ stdlib('c') }} - - cmake >=3.18 - ninja - - meson + - meson >=1.3 + - if: linux + then: patchelf # meson-python needs it for wheels on Linux - if: build_platform != target_platform then: - python @@ -40,12 +40,9 @@ requirements: host: - python - pip - - setuptools >=65 - - wheel + - meson-python >=0.15 - cython >=3 - numpy - - meson - - ninja - sundials >=6.2 - eigen - libblas diff --git a/cpp_tests/meson.build b/cpp_tests/meson.build new file mode 100644 index 00000000..28756c23 --- /dev/null +++ b/cpp_tests/meson.build @@ -0,0 +1,84 @@ +# C++ tests: meson setup build -Dpython=disabled -Dtests=true -Dbuildtype=debug +# meson test -C build +# add -Db_sanitize=address or -Db_coverage=true as needed. + +if not sundials_dep.found() + error('the C++ tests need SUNDIALS (-Dcvode=enabled)') +endif + +# the pinned submodule if checked out, else a system GoogleTest +if fs.is_file('gtest/googletest/src/gtest-all.cc') + gtest_inc = include_directories('gtest/googletest/include', 'gtest/googletest', is_system: true) + gtest_lib = static_library( + 'gtest', + 'gtest/googletest/src/gtest-all.cc', + 'gtest/googletest/src/gtest_main.cc', + include_directories: gtest_inc, + dependencies: dependency('threads'), + ) + gtest_dep = declare_dependency( + link_with: gtest_lib, + include_directories: gtest_inc, + dependencies: dependency('threads'), + ) +else + gtest_dep = dependency('gtest', main: true) +endif + +test_main = executable( + 'test_main', + 'source/test_aatopology.cpp', + 'source/test_array.cpp', + 'source/test_base_potential.cpp', + 'source/test_blj.cpp', + 'source/test_cell_list_minimize.cpp', + 'source/test_cell_lists.cpp', + 'source/test_cell_lists_hs_wca.cpp', + 'source/test_check_copy_assignment_cvode.cpp', + 'source/test_combinations.cpp', + 'source/test_combined_potential.cpp', + 'source/test_cvode.cpp', + 'source/test_distance.cpp', + 'source/test_eigenvalues.cpp', + 'source/test_energy_change.cpp', + 'source/test_fire.cpp', + 'source/test_frozen.cpp', + 'source/test_gaussianpot.cpp', + 'source/test_generic_mixed_descent.cpp', + 'source/test_gradient_descent.cpp', + 'source/test_harmonic.cpp', + 'source/test_hoomd.cpp', + 'source/test_inversepower.cpp', + 'source/test_inversepower_hs.cpp', + 'source/test_inversepower_stillinger.cpp', + 'source/test_inversepower_stillinger_cut.cpp', + 'source/test_inversepower_stillinger_cut_quad.cpp', + 'source/test_lbfgs.cpp', + 'source/test_lj.cpp', + 'source/test_lowest_eig_potential.cpp', + 'source/test_matrix.cpp', + 'source/test_mixed_descent.cpp', + 'source/test_mixed_descent_extended_potential.cpp', + 'source/test_morse.cpp', + 'source/test_newton.cpp', + 'source/test_ngt.cpp', + 'source/test_pressure.cpp', + 'source/test_pspin.cpp', + 'source/test_rattlers.cpp', + 'source/test_rotations.cpp', + 'source/test_utils.cpp', + 'source/test_vecn.cpp', + 'source/test_wca.cpp', + # not in the Python build yet, see pele/optimize + '../source/newton_trust.cpp', + dependencies: [pele_dep, gtest_dep, dependency('blas', required: false)], + build_rpath: env_rpath, +) +# the tests write their output files (trajectories etc.) to the working directory +test('pele C++ tests', test_main, workdir: meson.current_build_dir(), timeout: 1800) + +# benchmarks: ninja -C build bench_lj ... +foreach b : ['bench_cell_lists', 'bench_cell_lists_alt', 'bench_lj', 'bench_inversepower', 'bench_minimization'] + executable(b, 'source/benchmarks' / b + '.cpp', dependencies: pele_dep, build_rpath: env_rpath, + build_by_default: false) +endforeach diff --git a/cpp_tests/source/CMakeLists.txt b/cpp_tests/source/CMakeLists.txt deleted file mode 100644 index e9379720..00000000 --- a/cpp_tests/source/CMakeLists.txt +++ /dev/null @@ -1,140 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -project(pele_tests) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -enable_language(CXX) - -# TODO: Add a build type option specifically for memory leak testing -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Debug) -endif() - -message("CMAKE_BUILD_TYPE is ${CMAKE_BUILD_TYPE}") - -# include LAPACK AND BLAS -find_package(BLAS REQUIRED) -find_package(LAPACK REQUIRED) - -# set OpenMP -find_package(OpenMP) -message("${OpenMP_C_FLAGS}") -message("${OpenMP_CXX_FLAGS}") - -if(OPENMP_FOUND) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") -endif() - -if(CMAKE_BUILD_TYPE STREQUAL Debug) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb3 -O0") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ggdb3 -O0") - if(NOT APPLE) - # Apple does not support libsanitizer - # See https://github.com/orgs/Homebrew/discussions/3384#discussioncomment-6263471 - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -static-libasan") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -static-libasan") - endif(NOT APPLE) -elseif(CMAKE_BUILD_TYPE STREQUAL Release) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -DNDEBUG") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -DNDEBUG") -elseif(CMAKE_BUILD_TYPE STREQUAL Coverage) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g3 -DNDEBUG -fprofile-arcs -ftest-coverage") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g3 -DNDEBUG -fprofile-arcs -ftest-coverage") -endif() - -message(CMAKE_CONFIGURATION_TYPES ${CMAKE_CONFIGURATION_TYPES}) - -# find conda path for python header -set(PYTHON_INCLUDE_DIR $ENV{CONDA_PREFIX}/include/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}) -message("PYTHON INCLUDE DIR" ${PYTHON_INCLUDE_DIR}) - -set(EXT_INSTALL_PATH "${CMAKE_SOURCE_DIR}/../../extern/install") - -set(LAPACKE_INCLUDE_DIR ${EXT_INSTALL_PATH}/include/Eigen/src/misc) - -# set(SUNDIALS_EXTRA_LIBS -lm /home/praharsh/anaconda3/x86_64-conda_cos6-linux-gnu/sysroot/usr/lib/librt.so CACHE STRING "Additional libraries") -set(SUNDIALS_INCLUDE_DIR - ${EXT_INSTALL_PATH}/include - CACHE PATH "Location of SUNDIALS header files") -set(SUNDIALS_LIBRARY_DIR - ${EXT_INSTALL_PATH}/lib - CACHE PATH "Location of SUNDIALS libraries") - -message("SUNDIALS_INCLUDE_DIR ${SUNDIALS_INCLUDE_DIR}") - -# Find the SUNDIALS libraries -find_library(SUNDIALS_SOLVER_LIB - sundials_cvode ${SUNDIALS_LIBRARY_DIR} - DOC "CVODE library") - -find_library(SUNDIALS_NVEC_LIB - sundials_nvecserial ${SUNDIALS_LIBRARY_DIR} - DOC "NVECTOR_SERIAL library") - -find_library(SUNDIALS_MANYVEC_LIB - sundials_nvecmanyvector ${SUNDIALS_LIBRARY_DIR} - DOC "NVECTOR_MANYVECTOR library") - -set(SUNDIALS_LIBRARIES - -L${SUNDIALS_LIBRARY_DIR} - ${SUNDIALS_SOLVER_LIB} - ${SUNDIALS_NVEC_LIB} - ${SUNDIALS_MANYVEC_LIB} - ${SUNDIALS_EXTRA_LIBS}) - -# Add and compile the gtest library -set(gtest_src ../gtest) -set(gtest_binary_dir googletest) -set(gtest_include ${gtest_src}/include/) -include_directories(${gtest_include}) -message("gtest include directory ${gtest_include}") -add_subdirectory(../gtest ${gtest_binary_dir}) -set(gtest_libs gtest gtest_main) -message("gtest_libs ${gtest_libs}") - -include_directories(${PYTHON_INCLUDE_DIR}) - -# set the pele include directory -set(pele_include ../../source) -include_directories(BEFORE SYSTEM ${LAPACKE_INCLUDE_DIR}) -include_directories(${pele_include}) -include_directories(${SUNDIALS_INCLUDE_DIR}) - -message("pele include directory ${pele_include}") - -# build the pele library -FILE(GLOB pele_sources ${pele_include}/*.c*) -add_library(pele_lib ${pele_sources}) - -# get all the source files -FILE(GLOB sources *.c*) - -add_executable(test_main ${sources}) - -target_link_libraries(test_main pele_lib gtest gtest_main pthread) -target_link_libraries(test_main ${SUNDIALS_LIBRARIES}) -target_link_libraries(test_main ${BLAS_LIBRARIES}) -target_link_libraries(test_main ${LAPACK_LIBRARIES}) -if(APPLE) - # Apple changed the ld linker in Xcode 15 which breaks pele. - # The classic linker can be chosen with -ld_classic flag. - # However, on older Xcode versions, this flag throws an error. - # Thus, we simply check here whether this flag exists. - # See https://www.scivision.dev/xcode-ld_classic/ - include(CheckLinkerFlag) - check_linker_flag(C "-ld_classic" CLASSIC_FLAG) - if(CLASSIC_FLAG) - target_link_options(test_main PUBLIC "LINKER:-ld_classic") - endif(CLASSIC_FLAG) -endif(APPLE) - -set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMakeModules) - -if(CMAKE_BUILD_TYPE STREQUAL "Coverage") - include(CodeCoverage) - setup_target_for_coverage(${PROJECT_NAME}_coverage test_main coverage) -endif() \ No newline at end of file diff --git a/cpp_tests/source/CMakeModules/CodeCoverage.cmake b/cpp_tests/source/CMakeModules/CodeCoverage.cmake deleted file mode 100644 index 47ceae03..00000000 --- a/cpp_tests/source/CMakeModules/CodeCoverage.cmake +++ /dev/null @@ -1,156 +0,0 @@ -# -# 2012-01-31, Lars Bilke -# - Enable Code Coverage -# -# 2013-09-17, Joakim Söderberg -# - Added support for Clang. -# - Some additional usage instructions. -# -# USAGE: - -# 0. (Mac only) If you use Xcode 5.1 make sure to patch geninfo as described here: -# http://stackoverflow.com/a/22404544/80480 -# -# 1. Copy this file into your cmake modules path. -# -# 2. Add the following line to your CMakeLists.txt: -# INCLUDE(CodeCoverage) -# -# 3. Set compiler flags to turn off optimization and enable coverage: -# SET(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") -# SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") -# -# 3. Use the function SETUP_TARGET_FOR_COVERAGE to create a custom make target -# which runs your test executable and produces a lcov code coverage report: -# Example: -# SETUP_TARGET_FOR_COVERAGE( -# my_coverage_target # Name for custom target. -# test_driver # Name of the test driver executable that runs the tests. -# # NOTE! This should always have a ZERO as exit code -# # otherwise the coverage generation will not complete. -# coverage # Name of output directory. -# ) -# -# 4. Build a Debug build: -# cmake -DCMAKE_BUILD_TYPE=Debug .. -# make -# make my_coverage_target -# -# - -# Check prereqs -FIND_PROGRAM( GCOV_PATH gcov ) -FIND_PROGRAM( LCOV_PATH lcov ) -FIND_PROGRAM( GENHTML_PATH genhtml ) -FIND_PROGRAM( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests) - -IF(NOT GCOV_PATH) - MESSAGE(FATAL_ERROR "gcov not found! Aborting...") -ENDIF() # NOT GCOV_PATH - -IF(NOT CMAKE_COMPILER_IS_GNUCXX) - # Clang version 3.0.0 and greater now supports gcov as well. - MESSAGE(WARNING "Compiler is not GNU gcc! Clang Version 3.0.0 and greater supports gcov as well, but older versions don't.") - - IF(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - MESSAGE(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") - ENDIF() -ENDIF() # NOT CMAKE_COMPILER_IS_GNUCXX -SET(CMAKE_EXE_LINKER_FLAGS_COVERAGE - "" - CACHE STRING "Flags used for linking binaries during coverage builds." - FORCE ) -SET(CMAKE_SHARED_LINKER_FLAGS_COVERAGE - "" - CACHE STRING "Flags used by the shared libraries linker during coverage builds." - FORCE ) -MARK_AS_ADVANCED( - CMAKE_CXX_FLAGS_COVERAGE - CMAKE_C_FLAGS_COVERAGE - CMAKE_EXE_LINKER_FLAGS_COVERAGE - CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) - -IF ( NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "Coverage")) - MESSAGE( WARNING "Code coverage results with an optimized (non-Debug) build may be misleading" ) -ENDIF() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" - - -# Param _targetname The name of new the custom make target -# Param _testrunner The name of the target which runs the tests. -# MUST return ZERO always, even on errors. -# If not, no coverage report will be created! -# Param _outputname lcov output is generated as _outputname.info -# HTML report is generated in _outputname/index.html -# Optional fourth parameter is passed as arguments to _testrunner -# Pass them in list form, e.g.: "-j;2" for -j 2 -FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) - - IF(NOT LCOV_PATH) - MESSAGE(FATAL_ERROR "lcov not found! Aborting...") - ENDIF() # NOT LCOV_PATH - - IF(NOT GENHTML_PATH) - MESSAGE(FATAL_ERROR "genhtml not found! Aborting...") - ENDIF() # NOT GENHTML_PATH - - # Setup target - ADD_CUSTOM_TARGET(${_targetname} - - # Cleanup lcov - ${LCOV_PATH} --directory . --zerocounters - - # Run tests - COMMAND ${_testrunner} ${ARGV3} - - # Capturing lcov counters and generating report - COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info --ignore-errors usage,inconsistent,empty,count,mismatch --rc geninfo_unexecuted_blocks=1 - COMMAND ${LCOV_PATH} --remove ${_outputname}.info "${CMAKE_SOURCE_DIR}/cpp_tests/*" "${CMAKE_SOURCE_DIR}/extern/*" "/usr/*" "*/cpp_tests/*" "*/extern/*" "*/Eigen/*" "*gtest*" "*gmock*" "*test_*" "*/source/test_*" --output-file ${_outputname}.info.cleaned --ignore-errors unused,empty,count,mismatch - COMMAND ${LCOV_PATH} --list ${_outputname}.info.cleaned || echo "Listing coverage files failed" - COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned --ignore-errors empty - COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info - - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." - ) - - # Show info where to find the report - ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD - COMMAND ; - COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report." - ) - -ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE - -# Param _targetname The name of new the custom make target -# Param _testrunner The name of the target which runs the tests -# Param _outputname cobertura output is generated as _outputname.xml -# Optional fourth parameter is passed as arguments to _testrunner -# Pass them in list form, e.g.: "-j;2" for -j 2 -FUNCTION(SETUP_TARGET_FOR_COVERAGE_COBERTURA _targetname _testrunner _outputname) - - IF(NOT PYTHON_EXECUTABLE) - MESSAGE(FATAL_ERROR "Python not found! Aborting...") - ENDIF() # NOT PYTHON_EXECUTABLE - - IF(NOT GCOVR_PATH) - MESSAGE(FATAL_ERROR "gcovr not found! Aborting...") - ENDIF() # NOT GCOVR_PATH - - ADD_CUSTOM_TARGET(${_targetname} - - # Run tests - ${_testrunner} ${ARGV3} - - # Running gcovr - COMMAND ${GCOVR_PATH} -x -r ${CMAKE_SOURCE_DIR} -e '${CMAKE_SOURCE_DIR}/tests/' -o ${_outputname}.xml - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMENT "Running gcovr to produce Cobertura code coverage report." - ) - - # Show info where to find the report - ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD - COMMAND ; - COMMENT "Cobertura code coverage report saved in ${_outputname}.xml." - ) - -ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE_COBERTURA diff --git a/cpp_tests/source/benchmarks/CMakeLists.txt b/cpp_tests/source/benchmarks/CMakeLists.txt deleted file mode 100644 index 8d5993c1..00000000 --- a/cpp_tests/source/benchmarks/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -add_executable(bench_cell_lists bench_cell_lists.cpp) -target_link_libraries(bench_cell_lists pele_lib) - -add_executable(bench_cell_lists_alt bench_cell_lists_alt.cpp) -target_link_libraries(bench_cell_lists_alt pele_lib) - -add_executable(bench_lj bench_lj.cpp) -target_link_libraries(bench_lj pele_lib) - -add_executable(bench_inversepower bench_inversepower.cpp) -target_link_libraries(bench_inversepower pele_lib) - -add_executable(bench_minimization bench_minimization.cpp) -target_link_libraries(bench_minimization pele_lib) diff --git a/cythonize.py b/cythonize.py deleted file mode 100755 index 88436d8e..00000000 --- a/cythonize.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/env python -""" cythonize - -Cythonize pyx files into C files as needed. - -Usage: cythonize [root_dir] - -Default [root_dir] is 'pele'. - -Checks pyx files to see if they have been changed relative to their -corresponding C files. If they have, then runs cython on these files to -recreate the C files. - -The script thinks that the pyx files have changed relative to the C files -by comparing hashes stored in a database file. - -Simple script to invoke Cython (and Tempita) on all .pyx (.pyx.in) -files; while waiting for a proper build system. Uses file hashes to -figure out if rebuild is needed. - -For now, this script should be run by developers when changing Cython files -only, and the resulting C files checked in, so that end-users (and Python-only -developers) do not get the Cython/Tempita dependencies. - -Originally written by Dag Sverre Seljebotn, and copied here from: - -https://raw.github.com/dagss/private-scipy-refactor/cythonize/cythonize.py - -Note: this script does not check any of the dependent C libraries; it only -operates on the Cython .pyx files. -""" - - -import os -import re -import sys -import hashlib -import subprocess - -HASH_FILE = "cythonize.dat" -DEFAULT_ROOT = "pele" - -# WindowsError is not defined on unix systems -try: - WindowsError -except NameError: - WindowsError = None - -_extra_flags = [] - -# -# Rules -# -def process_pyx(fromfile, tofile): - try: - from Cython.Compiler.Version import version as cython_version - try: - from packaging.version import Version as LooseVersion - except ImportError: - # Fallback - create a simple version comparison - def LooseVersion(version_str): - class SimpleVersion: - def __init__(self, v): - self.version = v - def __lt__(self, other): - # Simple string comparison for version - return self.version < other.version - return SimpleVersion(version_str) - - if LooseVersion(cython_version) < LooseVersion("3.0.0"): - raise Exception("Building pele requires Cython >= 3.0.0") - - except ImportError: - pass - - flags = ["--fast-fail"] - if tofile.endswith(".cxx"): - flags += ["--cplus"] - - if _extra_flags: - flags += _extra_flags - - try: - try: - # print("in dir " + os.getcwd()) - print(" ".join(["cython"] + flags + ["-o", tofile, fromfile])) - r = subprocess.call(["cython"] + flags + ["-o", tofile, fromfile]) - if r != 0: - raise Exception("Cython failed") - except OSError: - # There are ways of installing Cython that don't result in a cython - # executable on the path, see gh-2397. - r = subprocess.call( - [ - sys.executable, - "-c", - "import sys; from Cython.Compiler.Main import " - "setuptools_main as main; sys.exit(main())", - ] - + flags - + ["-o", tofile, fromfile] - ) - if r != 0: - raise Exception("Cython failed") - except OSError: - raise OSError("Cython needs to be installed") - - -def process_tempita_pyx(fromfile, tofile): - import tempita - - with open(fromfile, "rb") as f: - tmpl = f.read() - pyxcontent = tempita.sub(tmpl) - assert fromfile.endswith(".pyx.in") - pyxfile = fromfile[: -len(".pyx.in")] + ".pyx" - with open(pyxfile, "wb") as f: - f.write(pyxcontent) - process_pyx(pyxfile, tofile) - - -rules = { - # fromext : function - ".pyx": process_pyx, - ".pyx.in": process_tempita_pyx, -} -# -# Hash db -# -def load_hashes(filename): - # Return { filename : (sha1 of input, sha1 of output) } - if os.path.isfile(filename): - hashes = {} - with open(filename) as f: - for line in f: - filename, inhash, outhash = line.split() - hashes[filename] = (inhash, outhash) - else: - hashes = {} - return hashes - - -def save_hashes(hash_db, filename): - with open(filename, "w") as f: - for key, value in sorted(hash_db.items()): - f.write("{} {} {}\n".format(key, value[0], value[1])) - - -def sha1_of_file(filename): - h = hashlib.sha1() - with open(filename, "rb") as f: - h.update(f.read()) - return h.hexdigest() - - -# -# Main program -# - - -def normpath(path): - path = path.replace(os.sep, "/") - if path.startswith("./"): - path = path[2:] - return path - - -def toolchain_version(): - """generated sources depend on the Cython and NumPy versions too, so building - in a different environment must regenerate them""" - versions = [] - for mod in ("Cython", "numpy"): - try: - versions.append(__import__(mod).__version__) - except ImportError: - versions.append("none") - return " ".join(versions) - - -def get_hash(frompath, topath): - from_hash = hashlib.sha1((sha1_of_file(frompath) + toolchain_version()).encode()).hexdigest() - to_hash = sha1_of_file(topath) if os.path.exists(topath) else None - return (from_hash, to_hash) - - -def process(path, fromfile, tofile, processor_function, hash_db): - """cythonize one file if it changed; returns (key, new hash) or None""" - fullfrompath = os.path.join(path, fromfile) - fulltopath = os.path.join(path, tofile) - current_hash = get_hash(fullfrompath, fulltopath) - if current_hash == hash_db.get(normpath(fullfrompath), None): - print("%s has not changed" % fullfrompath) - return None - print("Processing %s" % fullfrompath) - # absolute paths instead of chdir, so files can be processed in parallel - processor_function(os.path.abspath(fullfrompath), os.path.abspath(fulltopath)) - # changed target file, recompute hash - return normpath(fullfrompath), get_hash(fullfrompath, fulltopath) - - -def find_process_files(root_dir): - """find pyx files under root_dir and cythonize the changed ones in parallel""" - from concurrent.futures import ThreadPoolExecutor - - hash_db = load_hashes(HASH_FILE) - jobs = [] - for cur_dir, dirs, files in os.walk(root_dir): - for filename in files: - for fromext, function in list(rules.items()): - if filename.endswith(fromext): - toext = ".c" - with open(os.path.join(cur_dir, filename), "rb") as f: - data = f.read() - m = re.search( - rb"^\s*#\s*distutils:\s*language\s*=\s*c\+\+\s*$", - data, - re.I | re.M, - ) - if m: - toext = ".cxx" - tofile = filename[: -len(fromext)] + toext - jobs.append((cur_dir, filename, tofile, function)) - # threads are enough: each job is a cython subprocess - with ThreadPoolExecutor(max_workers=int(os.environ.get("PELE_JOBS", os.cpu_count() or 4))) as pool: - futures = [pool.submit(process, *job, hash_db) for job in jobs] - try: - for fut in futures: - res = fut.result() - if res is not None: - hash_db[res[0]] = res[1] - finally: - # keep hashes of the files that did succeed - save_hashes(hash_db, HASH_FILE) - - -def main(): - try: - root_dir = sys.argv[1] - except IndexError: - root_dir = DEFAULT_ROOT - - if len(sys.argv) > 2: - global _extra_flags - for f in sys.argv[2:]: - _extra_flags.append(f) - find_process_files(root_dir) - - -if __name__ == "__main__": - main() diff --git a/extern/eigen b/extern/eigen deleted file mode 160000 index 4be28702..00000000 --- a/extern/eigen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4be28702672e5b56cb2b6d840a212ef139a4e9f0 diff --git a/extern/sun_inst.sh b/extern/sun_inst.sh deleted file mode 100755 index b951c0cb..00000000 --- a/extern/sun_inst.sh +++ /dev/null @@ -1,31 +0,0 @@ -# Script to install sundials for pele -# first argument is the CMake build type: Release (default), Debug, RelWithDebInfo. -# Note: a Debug sundials is built without optimization and makes the CVODE optimizers several times slower. - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -c_d=$SCRIPT_DIR/install - -cd sundials -mkdir build -cd build - - -# note the newlines are important - -cmake -DCMAKE_INSTALL_PREFIX=$c_d \ - -DCMAKE_BUILD_TYPE=${1:-Release} \ - -DBUILD_ARKODE=OFF \ - -DBUILD_CVODES=OFF \ - -DBUILD_IDA=OFF \ - -DBUILD_IDAS=OFF \ - -DBUILD_KINSOL=OFF \ - -DENABLE_OPENMP=OFF \ - -DBUILD_STATIC_LIBS=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DCMAKE_CXX_STANDARD=17 \ - -DSUNDIALS_INDEX_SIZE=32 \ - .. -make install -cd .. -rm -rf build diff --git a/extern/sundials b/extern/sundials deleted file mode 160000 index c298f0f5..00000000 --- a/extern/sundials +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c298f0f56c48eb8ce687d399e981ce09b3d0c36e diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..b3df6c4b --- /dev/null +++ b/meson.build @@ -0,0 +1,299 @@ +project( + 'pele', + 'c', 'cpp', + version: '0.1.0', + meson_version: '>=1.3', + default_options: [ + 'buildtype=release', + 'b_ndebug=if-release', + 'cpp_std=c++20', + ], +) + +fs = import('fs') +cc = meson.get_compiler('cpp') +# meson always runs under a Python; used for configure-time helpers only +helper_py = import('python').find_installation() +with_python = get_option('python').allowed() + +if host_machine.system() == 'darwin' and cc.get_id() != 'gcc' + error('pele needs GCC on macOS (Apple clang has no OpenMP or Fortran): brew install gcc, then build with CC=gcc-15 CXX=g++-15 FC=gfortran-15') +endif + +native_args = get_option('native') and get_option('buildtype') != 'debug' ? ['-march=native'] : [] +add_project_arguments(native_args, language: ['c', 'cpp', 'fortran']) +omp_dep = dependency('openmp') + +# SUNDIALS, Eigen and LAPACK come from the active conda env if there is one, else the system. +# The env's shared libraries are found at run time through an rpath. +conda_prefix = run_command(helper_py, '-c', 'import os; print(os.environ.get("CONDA_PREFIX", ""))', check: true).stdout().strip() +prefixes = conda_prefix != '' ? [conda_prefix] : [] +env_rpath = conda_prefix != '' ? conda_prefix / 'lib' : '' + +eigen_inc = [] +foreach p : prefixes + foreach d : [p / 'include', p / 'include' / 'eigen3'] + if eigen_inc.length() == 0 and fs.is_dir(d / 'Eigen') + eigen_inc = [d] + endif + endforeach +endforeach +if eigen_inc.length() > 0 + eigen_dep = declare_dependency(include_directories: include_directories(eigen_inc, is_system: true)) +else + eigen_dep = dependency('eigen3') +endif + +pele_lib_sources = files( + 'source/aatopology.cpp', + 'source/atlj.cpp', + 'source/backtracking.cpp', + 'source/bracketing.cpp', + 'source/lbfgs.cpp', + 'source/linesearch.cpp', + 'source/modified_fire.cpp', + 'source/more_thuente.cpp', + 'source/nwpele.cpp', + 'source/pressure_tensor.cpp', + 'source/rotations.cpp', + 'source/xsum.c', + 'source/pbinary.c', +) +pele_lib_deps = [eigen_dep, omp_dep] + +sundials_dep = disabler() +if not get_option('cvode').disabled() + lib_dirs = [] + inc_dirs = [] + foreach p : prefixes + lib_dirs += p / 'lib' + inc_dirs += p / 'include' + endforeach + cvode_lib = cc.find_library('sundials_cvode', dirs: lib_dirs, required: get_option('cvode')) + if cvode_lib.found() + sundials_libs = [cvode_lib, cc.find_library('sundials_nvecserial', dirs: lib_dirs)] + # split out in sundials 7 + sundials_libs += cc.find_library('sundials_core', dirs: lib_dirs, required: false) + sundials_inc = [] + foreach d : inc_dirs + if sundials_inc.length() == 0 and fs.is_file(d / 'cvode' / 'cvode.h') + sundials_inc = [d] + endif + endforeach + # the mixed descent optimizers call LAPACK's Cholesky (dpotrf_, dpotrs_) + lapack_dep = cc.find_library('lapack', dirs: lib_dirs, required: false) + if not lapack_dep.found() + lapack_dep = dependency('lapack', 'openblas') + endif + sundials_dep = declare_dependency( + dependencies: sundials_libs + [lapack_dep], + include_directories: include_directories(sundials_inc, is_system: true), + ) + # the Fortran prototypes for those calls. lapack.h ships with the LAPACKE headers, + # but only liblapack is linked: nothing calls the LAPACKE_* C interface. + if not cc.has_header('lapack.h', dependencies: sundials_dep) + error('lapack.h not found. Install it with conda (blas-devel), apt (liblapacke-dev) or homebrew (openblas).') + endif + pele_lib_sources += files( + 'source/cvode.cpp', + 'source/extended_mixed_descent.cpp', + 'source/mxd_end_only.cpp', + 'source/newton.cpp', + 'source/newton_with_extension.cpp', + 'source/generic_mixed_descent.cpp', + ) + pele_lib_deps += sundials_dep + endif +endif + +pele_inc = include_directories('source', is_system: true) +pele_lib = static_library( + 'pele_lib', + pele_lib_sources, + include_directories: pele_inc, + dependencies: pele_lib_deps, + cpp_args: ['-Wall', '-Wextra', '-pedantic'], + pic: true, +) +pele_dep = declare_dependency( + link_with: pele_lib, + include_directories: pele_inc, + dependencies: pele_lib_deps, +) + +if with_python +add_languages('cython', 'fortran', native: false) +py = import('python').find_installation(pure: false) +py_dep = py.dependency() + +# Extensions built by a g++ newer than the libstdc++ that Python loads (e.g. system gcc +# with an older conda env) fail at import with "CXXABI_x not found". Refuse to build. +if host_machine.system() == 'linux' and cc.get_id() == 'gcc' + compiler_libstdcxx = run_command(cc.cmd_array(), '-print-file-name=libstdc++.so.6', check: true).stdout().strip() + libstdcxx_check = run_command(py, '-c', ''' +import ctypes, os, sys +ctypes.CDLL("libstdc++.so.6") +loaded = next(l.split()[-1] for l in open("/proc/self/maps") if "libstdc++.so" in l) +version = lambda p: tuple(int(x) for x in os.path.realpath(p).rsplit(".so.", 1)[1].split(".")) +if version(sys.argv[1]) > version(loaded): + print(os.path.realpath(sys.argv[1]), os.path.realpath(loaded)) +''', compiler_libstdcxx, check: true).stdout().strip() + if libstdcxx_check != '' + paths = libstdcxx_check.split(' ') + error('''The C++ compiler's runtime @0@ is newer than the one Python loads, @1@, +so pele would fail to import. Update the environment's runtime to match the compiler, +e.g. conda install -c conda-forge "libstdcxx-ng>=$(gcc -dumpversion)".'''.format(paths[0], paths[1])) + endif +endif + +incdir_numpy = run_command(py, '-c', 'import numpy; print(numpy.get_include())', check: true).stdout().strip() +incdir_f2py = run_command(py, '-c', 'import numpy.f2py; print(numpy.f2py.get_include())', check: true).stdout().strip() +np_dep = declare_dependency( + include_directories: include_directories(incdir_numpy), + compile_args: ['-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION'], +) + +cython_args = ['-X', 'c_string_type=unicode', '-X', 'c_string_encoding=utf-8', + '-I', meson.project_source_root()] + +# Cython extensions (C++ unless listed as C). The cvode ones need SUNDIALS. +cython_c = ['pele/optimize/_cython_lbfgs', 'pele/potentials/_cython_tools'] +cython_cpp = [ + 'pele/angleaxis/_cpp_aa', + 'pele/distance/_get_distance_cpp', + 'pele/distance/_put_in_box_cpp', + 'pele/optimize/_gradient_descent_cpp', + 'pele/optimize/_cosine_gradient_descent_cpp', + 'pele/optimize/_lbfgs_cpp', + 'pele/optimize/_modified_fire_cpp', + 'pele/optimize/_pele_opt', + 'pele/potentials/_frenkel', + 'pele/potentials/_frozen_dof', + 'pele/potentials/_harmonic_cpp', + 'pele/potentials/_hs_wca_cpp', + 'pele/potentials/_inversepower_cpp', + 'pele/potentials/_inversepower_hs_cpp', + 'pele/potentials/_inversepower_stillinger_cpp', + 'pele/potentials/_inversepower_stillinger_cut_cpp', + 'pele/potentials/_inversepower_stillinger_cut_quad', + 'pele/potentials/_lj_cpp', + 'pele/potentials/_morse_cpp', + 'pele/potentials/_pele', + 'pele/potentials/_pspin_spherical_cpp', + 'pele/potentials/_pythonpotential', + 'pele/potentials/_radial_gaussian_cpp', + 'pele/potentials/_sumgaussianpot_cpp', + 'pele/potentials/_wca_cpp', + 'pele/potentials/atlj', + 'pele/potentials/combine_potentials', + 'pele/potentials/cpp_test_functions', + 'pele/rates/_ngt_cpp', + 'pele/utils/_cpp_utils', + 'pele/utils/_pressure_tensor', +] +cython_cvode = [ + 'pele/optimize/_mxd_end_only', + 'pele/optimize/cvode_opt', + 'pele/optimize/extended_mixed_descent', + 'pele/optimize/generic_mixed_descent', +] +if sundials_dep.found() + cython_cpp += cython_cvode +endif + +foreach m : cython_c + cython_cpp + py.extension_module( + fs.name(m), + m + '.pyx', + subdir: fs.parent(m), + dependencies: [pele_dep, py_dep, np_dep], + cython_args: cython_args, + override_options: m in cython_c ? [] : ['cython_language=cpp'], + build_rpath: env_rpath, + install_rpath: env_rpath, + install: true, + ) +endforeach + +# f2py modules: f2py writes module.c and -f2pywrappers.f for each +fortranobject = static_library( + 'fortranobject', + incdir_f2py / 'fortranobject.c', + dependencies: [py_dep, np_dep], + include_directories: include_directories(incdir_f2py), + pic: true, +) +f2py_dep = declare_dependency( + link_with: fortranobject, + include_directories: include_directories(incdir_f2py), + dependencies: [py_dep, np_dep], +) +fortran_modules = [ + 'pele/mindist/minperm', + 'pele/optimize/mylbfgs_updatestep', + 'pele/potentials/fortran/AT', + 'pele/potentials/fortran/ljpshiftfort', + 'pele/potentials/fortran/lj', + 'pele/potentials/fortran/ljcut', + 'pele/potentials/fortran/lj_hess', + 'pele/potentials/fortran/morse', + 'pele/potentials/fortran/scdiff_periodic', + 'pele/potentials/fortran/FinSin', + 'pele/potentials/fortran/gupta', + 'pele/utils/_fortran_utils', + 'pele/transition_states/_orthogoptf', + 'pele/transition_states/_NEB_utils', + 'pele/angleaxis/_aadist', + 'pele/accept_tests/_spherical_container', +] +foreach m : fortran_modules + name = fs.name(m) + wrappers = custom_target( + name + '_f2py', + input: m + '.f90', + output: [name + 'module.c', name + '-f2pywrappers.f'], + command: [py, '-m', 'numpy.f2py', '@INPUT@', '-m', name, '--lower', '--build-dir', '@OUTDIR@', '--quiet'], + ) + py.extension_module( + name, + [m + '.f90', wrappers], + subdir: fs.parent(m), + dependencies: [f2py_dep], + link_language: 'fortran', + install: true, + ) +endforeach + +# pure Python files and package data +package_files = run_command(py, '-c', ''' +import os +data = (".py", ".pxd", ".pxi", ".xyz", ".xyzdr", ".data", ".sqlite") +for root, dirs, files in os.walk("pele"): + dirs[:] = sorted(d for d in dirs if d != "__pycache__") + for f in sorted(files): + p = os.path.join(root, f) + if p == os.path.join("pele", "version.py"): + continue + if (f.endswith(data) or f.startswith("points") or f in ("coords", "data") + or os.path.basename(root) == "non_additive_test_data"): + print(p) +''', check: true).stdout().strip().split('\n') +py.install_sources(package_files, preserve_path: true) + +# C++ sources and headers for extensions built against pele (see pele.get_include()) +install_subdir('source', install_dir: py.get_install_dir() / 'pele', + exclude_directories: ['gmin'], exclude_files: ['README.rst', 'CODING_STYLE.rst']) + +git_revision = run_command('git', 'rev-parse', 'HEAD', check: false) +configure_file( + output: 'version.py', + # layout (git_revision on the 3rd line) is parsed by downstream scripts + command: [py, '-c', 'import sys; open(sys.argv[1], "w").write("\\n# generated by meson.build\\ngit_revision = \'" + sys.argv[2] + "\'\\n")', + '@OUTPUT@', git_revision.returncode() == 0 ? git_revision.stdout().strip() : 'Unknown'], + install_dir: py.get_install_dir() / 'pele', +) +endif + +if get_option('tests') + subdir('cpp_tests') +endif diff --git a/meson.options b/meson.options new file mode 100644 index 00000000..f081e781 --- /dev/null +++ b/meson.options @@ -0,0 +1,8 @@ +option('cvode', type: 'feature', value: 'enabled', + description: 'CVODE/BDF optimizers (needs SUNDIALS). Needed for attractor identification.') +option('native', type: 'boolean', value: true, + description: 'Compile with -march=native (non-portable binaries)') +option('python', type: 'feature', value: 'enabled', + description: 'Python package (Cython and f2py extensions). Disable to build only the C++ tests.') +option('tests', type: 'boolean', value: false, + description: 'C++ tests (GoogleTest, from the cpp_tests/gtest submodule or the system). Run with meson test.') diff --git a/pyproject.toml b/pyproject.toml index 64108b9e..f1d9b437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,12 @@ [build-system] requires = [ - "setuptools>=65", - "wheel", - "numpy>=1.26", # f2py --backend meson - "cython>=3", - "cmake>=3.18", - "meson", # f2py backend + "meson-python>=0.15", + "meson>=1.3", "ninja", + "numpy>=1.26", + "cython>=3", ] -build-backend = "setuptools.build_meta" +build-backend = "mesonpy" [project] name = "pele" diff --git a/setup.py b/setup.py deleted file mode 100644 index 1b8a38f5..00000000 --- a/setup.py +++ /dev/null @@ -1,343 +0,0 @@ -"""Build pele. Works with `pip install .` and `python setup.py build_ext -i`. - -The Cython/Fortran/C++ extensions are compiled by CMake (from CMakeLists.txt.in) -inside build_ext; setuptools then just copies the resulting libraries. - -Options (command-line flags for direct `setup.py` use, env vars for pip): - -j N / PELE_JOBS parallel make jobs (default: all cores) - -c COMPILER unix (default) or intel - --build-type / PELE_BUILD_TYPE Release (default), Debug, RelWithDebInfo, MemCheck, Greene - --with-cvode / PELE_WITH_CVODE 1 (default) builds the CVODE/BDF optimizers (needs SUNDIALS) - --native / PELE_NATIVE 1 (default) adds -march=native; set 0 for portable binaries -""" -import argparse -import os -import shlex -import shutil -import subprocess -import sys -import sysconfig - -from setuptools import Extension, find_packages, setup -from setuptools.command.build_ext import build_ext as old_build_ext -from setuptools.command.build_py import build_py - -encoding = "utf-8" -parser = argparse.ArgumentParser(add_help=False) -parser.add_argument("-j", type=int, default=int(os.environ.get("PELE_JOBS", os.cpu_count() or 4))) -parser.add_argument("-c", "--compiler", type=str, default=None) -parser.add_argument("--opt-report", action="store_true", default=False, - help="Print optimization report (for Intel compiler)") -parser.add_argument("--build-type", type=str, default=os.environ.get("PELE_BUILD_TYPE", "Release"), - help="Release, Debug, RelWithDebInfo, MemCheck, Greene") -parser.add_argument("--with-cvode", type=int, default=int(os.environ.get("PELE_WITH_CVODE", 1)), - help="Build with CVODE. Needed for attractor identification.") -parser.add_argument("--native", type=int, default=int(os.environ.get("PELE_NATIVE", 1)), - help="Compile with -march=native (non-portable binaries)") -jargs, remaining_args = parser.parse_known_args(sys.argv) - -if not jargs.compiler or jargs.compiler in ("unix", "gnu", "gcc"): - idcompiler = "unix" -elif jargs.compiler in ("intelem", "intel", "icc", "icpc"): - idcompiler = "intel" -else: - raise ValueError("unknown compiler " + jargs.compiler) -# Only add the option back if it was really set (setup.py install does not allow -c) -if jargs.compiler: - remaining_args += ["-c", idcompiler] -sys.argv = remaining_args - -build_type = jargs.build_type -with_cvode = jargs.with_cvode - -common_args = ["-std=c++2a", "-Wall", "-Wextra", "-pedantic", "-fPIC", "-D_GLIBCXX_USE_CXX11_ABI=1"] -build_type_args = { - "Release": ["-O3", "-DNDEBUG"], - "Greene": ["-O3", "-DNDEBUG", "-unroll", "-ip", "-axCORE-AVX512", "-qopenmp", - "-qopt-report-stdout", "-qopt-report-phase=openmp"], - "Debug": ["-ggdb3", "-O0"], - "RelWithDebInfo": ["-g", "-O3"], - "MemCheck": ["-g", "-O0", "-fsanitize=address", "-fsanitize=leak"], -} -if build_type not in build_type_args: - raise ValueError("Unknown build type: " + build_type) -# env CXXFLAGS first (conda-forge hardening/arch flags), ours after so they win -cmake_compiler_extra_args = ( - shlex.split(os.environ.get("CXXFLAGS", "")) + common_args + build_type_args[build_type] -) -if jargs.native and build_type in ("Release", "RelWithDebInfo"): - cmake_compiler_extra_args += ["-march=native"] -if idcompiler == "unix": - cmake_compiler_extra_args += ["-fopenmp"] -else: - cmake_compiler_extra_args += ["-qopenmp"] - if jargs.opt_report: - cmake_compiler_extra_args += ["-qopt-report=5"] - -cmake_build_dir = "build/cmake" - -fortran_files = [ - "pele/mindist/minperm.f90", - "pele/optimize/mylbfgs_updatestep.f90", - "pele/potentials/fortran/AT.f90", - "pele/potentials/fortran/ljpshiftfort.f90", - "pele/potentials/fortran/lj.f90", - "pele/potentials/fortran/ljcut.f90", - "pele/potentials/fortran/lj_hess.f90", - "pele/potentials/fortran/morse.f90", - "pele/potentials/fortran/scdiff_periodic.f90", - "pele/potentials/fortran/FinSin.f90", - "pele/potentials/fortran/gupta.f90", - "pele/utils/_fortran_utils.f90", - "pele/transition_states/_orthogoptf.f90", - "pele/transition_states/_NEB_utils.f90", - "pele/angleaxis/_aadist.f90", - "pele/accept_tests/_spherical_container.f90", -] - -c_files = [ - "pele/optimize/_cython_lbfgs.c", - "pele/potentials/_cython_tools.c", -] - -cxx_files = [ - "pele/angleaxis/_cpp_aa.cxx", - "pele/distance/_get_distance_cpp.cxx", - "pele/distance/_put_in_box_cpp.cxx", - "pele/optimize/_gradient_descent_cpp.cxx", - "pele/optimize/_cosine_gradient_descent_cpp.cxx", - "pele/optimize/_lbfgs_cpp.cxx", - "pele/optimize/_modified_fire_cpp.cxx", - "pele/optimize/_pele_opt.cxx", - "pele/potentials/_frenkel.cxx", - "pele/potentials/_frozen_dof.cxx", - "pele/potentials/_harmonic_cpp.cxx", - "pele/potentials/_hs_wca_cpp.cxx", - "pele/potentials/_inversepower_cpp.cxx", - "pele/potentials/_inversepower_hs_cpp.cxx", - "pele/potentials/_inversepower_stillinger_cpp.cxx", - "pele/potentials/_inversepower_stillinger_cut_cpp.cxx", - "pele/potentials/_inversepower_stillinger_cut_quad.cxx", - "pele/potentials/_lj_cpp.cxx", - "pele/potentials/_morse_cpp.cxx", - "pele/potentials/_pele.cxx", - "pele/potentials/_pspin_spherical_cpp.cxx", - "pele/potentials/_pythonpotential.cxx", - "pele/potentials/_radial_gaussian_cpp.cxx", - "pele/potentials/_sumgaussianpot_cpp.cxx", - "pele/potentials/_wca_cpp.cxx", - "pele/potentials/atlj.cxx", - "pele/potentials/combine_potentials.cxx", - "pele/potentials/cpp_test_functions.cxx", - "pele/rates/_ngt_cpp.cxx", - "pele/utils/_cpp_utils.cxx", - "pele/utils/_pressure_tensor.cxx", -] -if with_cvode: - cxx_files += [ - "pele/optimize/_mxd_end_only.cxx", - "pele/optimize/cvode_opt.cxx", - "pele/optimize/extended_mixed_descent.cxx", - "pele/optimize/generic_mixed_descent.cxx", - ] - - -def git_version(): - try: - out = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, - env={"PATH": os.environ.get("PATH", ""), "LC_ALL": "C"}) - return out.stdout.strip().decode("ascii") or "Unknown" - except OSError: - return "Unknown" - - -def write_version_py(filename="pele/version.py"): - with open(filename, "w") as f: - # layout (git_revision on the 3rd line) is parsed by downstream scripts - f.write("\n# THIS FILE IS GENERATED FROM SCIPY SETUP.PY\ngit_revision = '%s'\n" % git_version()) - - -def generate_cython(): - cwd = os.path.abspath(os.path.dirname(__file__)) - print("Cythonizing sources") - # need to pass cython the include directory so it can find the .pyx files - cython_flags = ["-I", os.path.abspath("pele/potentials"), "-v"] - debug_flags = [] - if build_type in ["Debug", "RelWithDebInfo", "MemCheck"]: - debug_flags = ["--gdb", "--annotate", "-X", "linetrace=True", "-X", "boundscheck=True", - "-X", "wraparound=False", "-X", "cdivision=False"] - cython3_flags = ["-X", "language_level=3", "-X", "c_string_type=unicode", - "-X", "c_string_encoding=utf-8"] - p = subprocess.call( - [sys.executable, os.path.join(cwd, "cythonize.py"), "pele"] - + cython_flags + debug_flags + cython3_flags, - cwd=cwd, - ) - if p != 0: - raise RuntimeError("Running cythonize failed!") - - -def which(name): - path = shutil.which(name) - if path is None: - raise RuntimeError("could not find " + name + " on PATH") - return path - - -def get_compiler_env(compiler_id): - """Environment and cmake args for the compilers. - - CC/CXX/FC from the environment (e.g. conda compilers) are respected. Otherwise - gcc is used, on macOS the newest homebrew gcc-N. - """ - env = os.environ.copy() - cmake_args = ["-DCMAKE_EXPORT_COMPILE_COMMANDS=1"] - # CMake only finds headers/libraries (sundials, eigen, lapack) under the active - # conda env by itself when conda's own compilers are used - prefixes = [env["CONDA_PREFIX"]] if env.get("CONDA_PREFIX") else [] - if compiler_id == "unix": - if sys.platform.startswith("darwin") and "CC" not in env: - version = next((v for v in range(20, 9, -1) if shutil.which(f"gcc-{v}")), None) - if version is None: - raise RuntimeError( - "Could not find a homebrew GNU compiler gcc-N (N=10..20) on PATH. " - "Install one or set CC, CXX and FC." - ) - env["CC"] = which(f"gcc-{version}") - env["CXX"] = which(f"g++-{version}") - # f2py looks at F90, cmake at FC - env["F90"] = env["FC"] = which(f"gfortran-{version}") - prefixes += [subprocess.check_output(["brew", "--prefix", p]).decode(encoding).strip() - for p in ("openblas", "gettext")] - env.setdefault("CC", "gcc") - env.setdefault("CXX", "g++") - elif compiler_id == "intel": - env["CC"], env["CXX"], env["AR"] = which("icc"), which("icpc"), which("xiar") - cmake_args.append("-DCMAKE_AR=" + env["AR"]) - else: - raise Exception("compiler id not known") - if prefixes: - cmake_args.append("-DCMAKE_PREFIX_PATH=" + ";".join(prefixes)) - cmake_args += ["-DCMAKE_C_COMPILER=" + env["CC"], "-DCMAKE_CXX_COMPILER=" + env["CXX"]] - if "FC" in env: - cmake_args.append("-DCMAKE_Fortran_COMPILER=" + env["FC"]) - return env, cmake_args - - -def get_ldflags(): - """linker flags for libpython (only used on macOS, see CMakeLists.txt.in)""" - getvar = sysconfig.get_config_var - libs = (getvar("LIBS") or "").split() + (getvar("SYSLIBS") or "").split() - if not getvar("Py_ENABLE_SHARED"): - libs.insert(0, "-L" + getvar("LIBDIR")) - if not getvar("PYTHONFRAMEWORK"): - # See https://github.com/kovidgoyal/kitty/issues/289#issuecomment-416040645 - libs.extend((getvar("LINKFORSHARED") or "").replace("-Wl,-stack_size,1000000", "").split()) - return " ".join(libs) - - -def write_cmakelists(): - """create CMakeLists.txt from CMakeLists.txt.in""" - import numpy as np - - with open("CMakeLists.txt.in") as fin: - cmake_txt = fin.read() - python_includes = {sysconfig.get_path("include"), sysconfig.get_path("platinclude")} - cmake_txt = cmake_txt.replace("__PYTHON_INCLUDE__", " ".join(sorted(python_includes))) - cmake_txt = cmake_txt.replace("__INCLUDE_SUNDIALS__", "ON" if with_cvode else "OFF") - cmake_txt = cmake_txt.replace("__NUMPY_INCLUDE__", np.get_include()) - cmake_txt = cmake_txt.replace("__PYTHON_LDFLAGS__", get_ldflags()) - cmake_txt = cmake_txt.replace( - "__COMPILER_EXTRA_ARGS__", '"{}"'.format(" ".join(cmake_compiler_extra_args)) - ) - with open("CMakeLists.txt", "w") as fout: - fout.write(cmake_txt) - fout.write("\n") - for fname in cxx_files + c_files: - fout.write("make_cython_lib(${CMAKE_CURRENT_SOURCE_DIR}/%s)\n" % fname) - for fname in fortran_files: - fout.write("make_fortran_lib(${CMAKE_CURRENT_SOURCE_DIR}/%s)\n" % fname) - - -def run_cmake(): - os.makedirs(cmake_build_dir, exist_ok=True) - print("\nrunning cmake in directory", cmake_build_dir) - cwd = os.path.abspath(os.path.dirname(__file__)) - env, cmake_args = get_compiler_env(idcompiler) - cmake_args += ["-DPYTHON_EXECUTABLE=" + sys.executable] - if shutil.which("ninja"): - cache = os.path.join(cmake_build_dir, "CMakeCache.txt") - # cmake refuses to switch generators in an existing build dir - if os.path.isfile(cache) and "CMAKE_GENERATOR:INTERNAL=Ninja\n" not in open(cache).read(): - os.remove(cache) - shutil.rmtree(os.path.join(cmake_build_dir, "CMakeFiles"), ignore_errors=True) - cmake_args += ["-G", "Ninja"] - if build_type == "Release": - # CMake picks the LTO-aware archiver (gcc-ar) for the static pele_lib - cmake_args += ["-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON"] - if subprocess.call(["cmake"] + cmake_args + [cwd], cwd=cmake_build_dir, env=env) != 0: - raise Exception("running cmake failed") - print("\nbuilding files in cmake directory, jobs:", jargs.j) - if subprocess.call(["cmake", "--build", ".", "-j", str(jargs.j)], cwd=cmake_build_dir, env=env) != 0: - raise Exception("building libraries with CMake failed") - print("finished building the extension modules with cmake\n") - - -class build_ext_precompiled(old_build_ext): - """Build everything with CMake, then copy each library (stored in - extension.sources[0]) to where setuptools expects the extension.""" - - def run(self): - generate_cython() - write_cmakelists() - run_cmake() - super().run() - - def build_extension(self, ext): - ext_path = self.get_ext_fullpath(ext.name) - pre_compiled_library = ext.sources[0] - if not os.path.isfile(pre_compiled_library): - raise RuntimeError( - "file does not exist: " + pre_compiled_library + " Did CMake not run correctly" - ) - os.makedirs(os.path.dirname(ext_path), exist_ok=True) - print("copying", pre_compiled_library, "to", ext_path) - shutil.copy2(pre_compiled_library, ext_path) - - -class build_py_with_source(build_py): - """also install the C++ sources/headers as pele/source, see pele.get_include()""" - - def run(self): - super().run() - dest = os.path.join(self.build_lib, "pele", "source") - shutil.rmtree(dest, ignore_errors=True) - shutil.copytree("source", dest, ignore=shutil.ignore_patterns("*.rst", "gmin")) - - -def module_name(fname): - return os.path.splitext(fname)[0].replace("/", ".") - - -# The `source` of each extension is the location of the library built by CMake. -# f2py libraries carry the python ABI suffix, e.g. minperm.cpython-312-x86_64-linux-gnu.so -ext_modules = [ - Extension(module_name(f), [os.path.join(cmake_build_dir, os.path.splitext(os.path.basename(f))[0] + ".so")]) - for f in cxx_files + c_files -] + [ - Extension(module_name(f), [os.path.join(cmake_build_dir, os.path.splitext(os.path.basename(f))[0] - + sysconfig.get_config_var("EXT_SUFFIX"))]) - for f in fortran_files -] - -# written before setup() so build_py installs the current one -write_version_py() - -# metadata lives in pyproject.toml -setup( - packages=find_packages(include=["pele", "pele.*"]), - package_data={"": ["*.xyz", "*.xyzdr", "*.data", "*.sqlite", "points*", "*.pxd", "*.pxi", - "coords", "data", "non_additive_test_data/*"]}, - ext_modules=ext_modules, - cmdclass=dict(build_ext=build_ext_precompiled, build_py=build_py_with_source), -) diff --git a/setup_with_cmake.py b/setup_with_cmake.py deleted file mode 100644 index c8188a41..00000000 --- a/setup_with_cmake.py +++ /dev/null @@ -1,2 +0,0 @@ -# kept for old instructions/scripts: `python setup_with_cmake.py build_ext -i` == `python setup.py build_ext -i` -exec(open(__file__.replace("setup_with_cmake.py", "setup.py")).read()) diff --git a/source/newton.cpp b/source/newton.cpp index 8e4aa0b9..0c6e8532 100644 --- a/source/newton.cpp +++ b/source/newton.cpp @@ -6,7 +6,6 @@ #include "pele/optimizer.hpp" // Lapack for cholesky extern "C" { -#include } using pele::Array; diff --git a/source/newton_with_extension.cpp b/source/newton_with_extension.cpp index 7715964a..d35c6ea7 100644 --- a/source/newton_with_extension.cpp +++ b/source/newton_with_extension.cpp @@ -6,7 +6,6 @@ #include "pele/optimizer.hpp" // Lapack for cholesky extern "C" { -#include } using pele::Array; diff --git a/source/pele/extended_mixed_descent.hpp b/source/pele/extended_mixed_descent.hpp index 3aa9f127..395acaa7 100644 --- a/source/pele/extended_mixed_descent.hpp +++ b/source/pele/extended_mixed_descent.hpp @@ -19,7 +19,7 @@ // Lapack for cholesky extern "C" { -#include +#include } // line search methods