From 2351fdc3bac539ca81ec545caa9b7ba2155f9587 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 12:51:37 -0500 Subject: [PATCH 01/52] feat: migrate pyalps bindings to nanobind --- .github/workflows/build_wheels.yml | 65 ++-- CMakeLists.txt | 99 +---- .../dmft/qmc/hybridization/hybmain.cpp | 16 +- .../dmft/qmc/interaction_expansion2/main.cpp | 15 +- bindings/python/pyalps/CMakeLists.txt | 139 +++++++ bindings/python/pyalps/README.md | 24 ++ bindings/python/pyalps/cpp/dict_to_params.hpp | 35 ++ .../python/pyalps/cpp/ngs/accumulator.cpp | 123 ++++++ bindings/python/pyalps/cpp/ngs/api.cpp | 32 ++ .../pyalps/cpp/ngs/extract_from_pyobject.hpp | 130 +++++++ bindings/python/pyalps/cpp/ngs/hdf5.cpp | 317 +++++++++++++++ bindings/python/pyalps/cpp/ngs/mcbase.cpp | 173 ++++++++ bindings/python/pyalps/cpp/ngs/observable.cpp | 81 ++++ .../python/pyalps/cpp/ngs/observables.cpp | 108 +++++ bindings/python/pyalps/cpp/ngs/params.cpp | 160 ++++++++ bindings/python/pyalps/cpp/ngs/random01.cpp | 24 ++ bindings/python/pyalps/cpp/ngs/result.cpp | 181 +++++++++ bindings/python/pyalps/cpp/ngs/results.cpp | 86 ++++ bindings/python/pyalps/cpp/numpy_compat.hpp | 95 +++++ bindings/python/pyalps/cpp/pyalea.cpp | 368 ++++++++++++++++++ bindings/python/pyalps/cpp/pymcdata.cpp | 329 ++++++++++++++++ bindings/python/pyalps/cpp/pytools.cpp | 52 +++ .../pyalps/cpp/save_observable_to_hdf5.hpp | 15 + .../python/pyalps/src}/pyalps/__init__.py | 9 + .../python/pyalps/src/pyalps/_ext/__init__.py | 4 + .../python/pyalps/src}/pyalps/alea.py | 0 .../python/pyalps/src}/pyalps/alea_detail.py | 0 .../python/pyalps/src}/pyalps/apptest.py | 0 .../python/pyalps/src}/pyalps/cxx.py | 28 +- .../python/pyalps/src}/pyalps/dataset.py | 0 .../pyalps/src}/pyalps/dict_intersect.py | 0 .../python/pyalps/src}/pyalps/fit_wrapper.py | 0 .../pyalps/src}/pyalps/floatwitherror.py | 0 .../python/pyalps/src}/pyalps/hdf5.py | 0 .../python/pyalps/src}/pyalps/hlist.py | 0 .../python/pyalps/src}/pyalps/lattice.py | 0 .../python/pyalps/src}/pyalps/load.py | 0 .../python/pyalps/src}/pyalps/math.py | 0 .../python/pyalps/src}/pyalps/maxent.py | 6 +- .../python/pyalps/src}/pyalps/mpi.py | 0 .../pyalps/src}/pyalps/mpl_setup_macosx.py | 0 .../python/pyalps/src}/pyalps/mpl_setup_qt.py | 0 .../python/pyalps/src}/pyalps/mpl_setup_tk.py | 0 .../python/pyalps/src}/pyalps/natural_sort.py | 0 .../python/pyalps/src}/pyalps/ngs.py | 24 +- .../python/pyalps/src}/pyalps/plot.py | 0 .../python/pyalps/src}/pyalps/plot_core.py | 0 .../pyalps/src}/pyalps/pyalps_config.py | 0 .../pyalps/src}/pyalps/pyalps_config.py.in | 2 +- .../python/pyalps/src}/pyalps/pytools.py | 0 .../python/pyalps/src}/pyalps/tools.py | 0 lib/pyalps/CMakeLists.txt | 155 -------- pyproject.toml | 82 +--- src/alps/CMakeLists.txt | 66 +--- src/alps/alea/mcanalyze.hpp | 1 - src/alps/ngs/numeric/vector.hpp | 30 +- test/CMakeLists.txt | 5 +- test/pyalps/test_binding_surface.py | 152 ++++++++ tool/maxent.cpp | 16 +- tutorials/CMakeLists.txt | 2 +- 60 files changed, 2762 insertions(+), 487 deletions(-) create mode 100644 bindings/python/pyalps/CMakeLists.txt create mode 100644 bindings/python/pyalps/README.md create mode 100644 bindings/python/pyalps/cpp/dict_to_params.hpp create mode 100644 bindings/python/pyalps/cpp/ngs/accumulator.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/api.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp create mode 100644 bindings/python/pyalps/cpp/ngs/hdf5.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/mcbase.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/observable.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/observables.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/params.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/random01.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/result.cpp create mode 100644 bindings/python/pyalps/cpp/ngs/results.cpp create mode 100644 bindings/python/pyalps/cpp/numpy_compat.hpp create mode 100644 bindings/python/pyalps/cpp/pyalea.cpp create mode 100644 bindings/python/pyalps/cpp/pymcdata.cpp create mode 100644 bindings/python/pyalps/cpp/pytools.cpp create mode 100644 bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp rename {lib => bindings/python/pyalps/src}/pyalps/__init__.py (73%) create mode 100644 bindings/python/pyalps/src/pyalps/_ext/__init__.py rename {lib => bindings/python/pyalps/src}/pyalps/alea.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/alea_detail.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/apptest.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/cxx.py (68%) rename {lib => bindings/python/pyalps/src}/pyalps/dataset.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/dict_intersect.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/fit_wrapper.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/floatwitherror.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/hdf5.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/hlist.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/lattice.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/load.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/math.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/maxent.py (91%) rename {lib => bindings/python/pyalps/src}/pyalps/mpi.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/mpl_setup_macosx.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/mpl_setup_qt.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/mpl_setup_tk.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/natural_sort.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/ngs.py (74%) rename {lib => bindings/python/pyalps/src}/pyalps/plot.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/plot_core.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/pyalps_config.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/pyalps_config.py.in (51%) rename {lib => bindings/python/pyalps/src}/pyalps/pytools.py (100%) rename {lib => bindings/python/pyalps/src}/pyalps/tools.py (100%) delete mode 100644 lib/pyalps/CMakeLists.txt create mode 100644 test/pyalps/test_binding_surface.py diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 5a89aa119..7b7fef3c3 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -26,16 +26,12 @@ jobs: steps: - uses: actions/checkout@v7 - # Install Fortran compiler based on OS - - name: Install dependencies - run: | - if [ "${{ matrix.plat.os }}" = "ubuntu-latest" ]; then - sudo apt-get update - sudo apt-get install -y gfortran - else - brew update - brew install gfortran - fi + - name: Restore compiler cache + uses: actions/cache@v4 + with: + path: _build/ccache + key: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}-${{ hashFiles('src/**', 'bindings/python/**', 'applications/**', 'tool/maxent*') }} + restore-keys: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}- - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 @@ -43,24 +39,43 @@ jobs: CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* CIBW_ARCHS: ${{ matrix.plat.arch }} CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} - - # Set Fortran compiler for all platforms - CIBW_ENVIRONMENT: "FC=gfortran" - - # macOS-specific settings + CIBW_ENVIRONMENT: > + ALPS_DIR={project}/_build/cibw-install/share/alps + CCACHE_DIR={project}/_build/ccache + CCACHE_NAMESPACE=pyalps-wheel + CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + CIBW_BEFORE_ALL_LINUX: > + dnf install -y ccache cmake hdf5-devel openmpi-devel lapack-devel ninja-build && + cmake -S {project} -B {project}/_build/cibw-alps -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + -DALPS_BUILD_LIBS_ONLY=ON + -DALPS_BUILD_TESTS=OFF + -DALPS_BUILD_EXAMPLES=OFF + -DALPS_BUILD_APPLICATIONS=OFF && + cmake --build {project}/_build/cibw-alps --target install -j2 + CIBW_BEFORE_ALL_MACOS: > + brew install ccache cmake hdf5 open-mpi ninja && + cmake -S {project} -B {project}/_build/cibw-alps -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + -DALPS_BUILD_LIBS_ONLY=ON + -DALPS_BUILD_TESTS=OFF + -DALPS_BUILD_EXAMPLES=OFF + -DALPS_BUILD_APPLICATIONS=OFF + -DHDF5_ROOT=${{ matrix.plat.homebrew }}/opt/hdf5 && + cmake --build {project}/_build/cibw-alps --target install -j2 CIBW_ENVIRONMENT_MACOS: > - Boost_ROOT_DIR=/Users/runner/work/ALPS/ALPS/boost_1_87_0 + ALPS_DIR={project}/_build/cibw-install/share/alps + CCACHE_DIR={project}/_build/ccache + CCACHE_NAMESPACE=pyalps-wheel + CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }} CXXFLAGS="-stdlib=libc++" -# CIBW_ENVIRONMENT: > - - # env: - # CIBW_SOME_OPTION: value - # ... - # with: - # package-dir: . - # output-dir: wheelhouse - # config-file: "{package}/pyproject.toml" + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: pytest -q {project}/test/pyalps - uses: actions/upload-artifact@v7 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 90985ca22..3924a5e2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,12 +28,6 @@ if (NOT_BUILD_SHARED_LIBS) else (NOT_BUILD_SHARED_LIBS) option(BUILD_SHARED_LIBS "Build shared libraries" ON) endif (NOT_BUILD_SHARED_LIBS) -if (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_PYTHON "Build ALPS python extentions" OFF) -else (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_PYTHON "Build ALPS python extentions" ON) -endif (NOT_ALPS_BUILD_PYTHON) - option(ALPS_BUILD_DEVELOPER_TOOLS "Build tools used by developers to maintain ALPS" OFF) option(ALPS_ENABLE_OPENMP "Enable OpenMP parallelization" OFF) option(ALPS_ENABLE_OPENMP_WORKER "Enable OpenMP worker support" OFF) @@ -46,7 +40,6 @@ option(ALPS_LINK_BOOST_TEST "Link Boost.test pre-built library" OFF) option(ALPS_INSTALL_BOOST_TEST "Install Boost Test framework" OFF) option(ALPS_NGS_USE_NEW_ALEA "Use the ALPS ngs-alea instead of just alea" OFF) option(ALPS_NGS_OPENMPI_ULFM "Userlevel failure mitigation is available" OFF) -option(ALPS_PYTHON_WHEEL "Build python wheel" OFF) option(ALPS_BUILD_LIBS_ONLY "Build only libraries" OFF) @@ -56,7 +49,6 @@ mark_as_advanced(ALPS_LINK_BOOST_TEST) mark_as_advanced(ALPS_INSTALL_BOOST_TEST) mark_as_advanced(ALPS_NGS_USE_NEW_ALEA) mark_as_advanced(ALPS_NGS_OPENMPI_ULFM) -mark_as_advanced(ALPS_PYTHON_WHEEL) mark_as_advanced(ALPS_BUILD_LIBS_ONLY) option(ALPS_USE_MKL_PARALLEL "Use parallel version of MKL" OFF) @@ -65,7 +57,6 @@ mark_as_advanced(ALPS_USE_MKL_PARALLEL) SET (APPLICATIONS_CAN_BE_BUILT ON) set(ALPS_BOOST_LIBRARY_NAME "boost" CACHE STRING "name of the boost library") -set(ALPS_BOOST_PYTHON_LIBRARY_NAME "boost_python" CACHE STRING "name of the boost library") set(ALPS_ENABLE_MPI ON CACHE BOOL "Enable MPI Parallelization") set(ALPS_INSTALL_HEADERS ON CACHE BOOL "Install headers for ALPS and all dependent libraries") set(ALPS_BUILD_EXAMPLES ON CACHE BOOL "Build ALPS examples") @@ -74,11 +65,6 @@ set(ALPS_BUILD_APPLICATIONS ${APPLICATIONS_CAN_BE_BUILT} CACHE BOOL "Build ALPS mark_as_advanced(ALPS_BOOST_LIBRARY_NAME) -if(ALPS_PYTHON_WHEEL) - set(ALPS_BUILD_APPLICATIONS ON) - set(ALPS_ENABLE_MPI OFF) -endif() - if(ALPS_BUILD_LIBS_ONLY) set(ALPS_BUILD_APPLICATIONS OFF) endif() @@ -136,10 +122,6 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) # set default CMAKE_INSTALL_PREFIX ###################################################################### -if (ALPS_BUILD_PYTHON) - find_package(PythonMod REQUIRED) # COMPONENTS Interpreter Development.Module ) -endif (ALPS_BUILD_PYTHON) - ###################################################################### # Version information ###################################################################### @@ -293,24 +275,9 @@ IF(HDF5_IS_PARALLEL) INCLUDE_DIRECTORIES(${MPI_INCLUDE_PATH}) ENDIF(HDF5_IS_PARALLEL) -# python - - -if (ALPS_BUILD_PYTHON) - set(PYTHON_SCRIPTDIR "${CMAKE_INSTALL_PREFIX}/lib/python") - find_package(PythonMod) -endif (ALPS_BUILD_PYTHON) - -IF (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) - include_directories(${PYTHON_NUMPY_INCLUDE_DIR}) - MESSAGE (STATUS "Numpy include in ${PYTHON_NUMPY_INCLUDE_DIR}") - SET(ALPS_HAVE_PYTHON ON) - INCLUDE_DIRECTORIES(${PYTHON_INCLUDE_DIRS}) - set(BUILD_BOOST_PYTHON TRUE) -ELSE (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) - set(BUILD_BOOST_PYTHON OFF) - SET(ALPS_HAVE_PYTHON OFF) -ENDIF (PYTHONLIBS_FOUND AND ALPS_BUILD_PYTHON) +# Python bindings are built by the standalone scikit-build-core project in +# bindings/python/pyalps. The C++ SDK deliberately has no Python dependency. +set(BUILD_BOOST_PYTHON OFF) # Boost Libraries find_package(BoostForALPS REQUIRED) @@ -413,12 +380,6 @@ endif(MPI_INCLUDE_DIR) if(HDF5_INCLUDE_DIR) list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${HDF5_INCLUDE_DIR}) endif(HDF5_INCLUDE_DIR) -if(PYTHON_INCLUDE_DIRS) - list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS}) -endif(PYTHON_INCLUDE_DIRS) -if(PYTHON_NUMPY_INCLUDE_DIR) - list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${PYTHON_NUMPY_INCLUDE_DIR}) -endif(PYTHON_NUMPY_INCLUDE_DIR) if(Boost_INCLUDE_DIR_CONFIG) list(APPEND ALPS_EXTRA_INCLUDE_DIRS ${Boost_INCLUDE_DIR_CONFIG}) endif(Boost_INCLUDE_DIR_CONFIG) @@ -450,10 +411,6 @@ endif(LAPACK_LIBRARIES) if(HDF5_LIBRARIES) list(APPEND ALPS_EXTRA_LIBRARIES ${HDF5_LIBRARIES}) endif(HDF5_LIBRARIES) -if(PYTHON_LIBRARY) - list(APPEND ALPS_EXTRA_LIBRARIES ${PYTHON_LIBRARY}) -endif(PYTHON_LIBRARY) - configure_file(cmake/ALPSConfig.cmake.in ${PROJECT_BINARY_DIR}/cmake/ALPSConfig.cmake @ONLY) configure_file(cmake/include.mk.in ${PROJECT_BINARY_DIR}/cmake/include.mk) @@ -469,22 +426,17 @@ write_basic_package_version_file( # installation ###################################################################### include(InstallRequiredSystemLibraries) -if(ALPS_PYTHON_WHEEL) - install(DIRECTORY lib/pyalps DESTINATION . COMPONENT python - FILES_MATCHING PATTERN "*.py" - ) - install(DIRECTORY lib/xml DESTINATION pyalps COMPONENT xml - FILES_MATCHING PATTERN "*.xsl" - ) - install(DIRECTORY ${PROJECT_BINARY_DIR}/lib/xml DESTINATION pyalps COMPONENT xml) -elseif(ALPS_BUILD_LIBS_ONLY) -else() - if (ALPS_INSTALL_HEADERS) set(ALPS_HEADER_DIR "include") install(DIRECTORY src/alps src/boost src/ietl src/mocasito COMPONENT headers DESTINATION ${ALPS_HEADER_DIR} FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp" ) + # ALPS' public headers include the vendored Boost.Numeric bindings. Install + # them as part of the C++ SDK so downstream consumers do not need the ALPS + # source tree on their include path. + install(DIRECTORY bindings/boost COMPONENT headers DESTINATION ${ALPS_HEADER_DIR} + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" PATTERN "*.ipp" + ) install(FILES ${PROJECT_BINARY_DIR}/src/alps/config.h ${PROJECT_BINARY_DIR}/src/alps/version.h DESTINATION ${ALPS_HEADER_DIR}/alps COMPONENT headers) endif(ALPS_INSTALL_HEADERS) @@ -492,12 +444,6 @@ install(DIRECTORY lib/xml DESTINATION ${ALPS_XML_PATH} COMPONENT xml FILES_MATCHING PATTERN "*.xsl" ) -if(ALPS_BUILD_PYTHON AND ALPS_PYTHON_LIB_DEST_ROOT) - install(DIRECTORY lib/pyalps DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT} COMPONENT python - FILES_MATCHING PATTERN "*.py" PATTERN "*.pyc" - ) -endif() - install(DIRECTORY ${PROJECT_BINARY_DIR}/lib/xml DESTINATION ${ALPS_XML_PATH} COMPONENT xml) add_subdirectory(cmake) @@ -513,43 +459,24 @@ install(FILES cmake/UseALPS.cmake install(FILES CITATION.md LICENSE.txt README.md DESTINATION share/alps COMPONENT libraries) -if(ALPS_BUILD_PYTHON AND ALPS_PYTHON_LIB_DEST_ROOT) - string(CONFIGURE [[ - set(PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@") - set(ALPS_PYTHON_LIB_DEST_ROOT "@ALPS_PYTHON_LIB_DEST_ROOT@") - message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}") - message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}") - message(STATUS "CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}") - message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") - message(STATUS "ALPS_PYTHON_LIB_DEST_ROOT: ${ALPS_PYTHON_LIB_DEST_ROOT}") - - configure_file(${PROJECT_SOURCE_DIR}/lib/pyalps/pyalps_config.py.in ${CMAKE_INSTALL_PREFIX}/${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps/pyalps_config.py) - ]] install_script @ONLY) - install(CODE ${install_script}) -endif() - -set(PYTHONEXEC python) - -endif() ###################################################################### # libraries ###################################################################### add_subdirectory(src/boost) add_subdirectory(src/alps) -add_subdirectory(lib/pyalps) ###################################################################### # programs ###################################################################### -if (NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if (NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(tool) if (ALPS_BUILD_EXAMPLES) add_subdirectory(example) endif (ALPS_BUILD_EXAMPLES) -endif (NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +endif (NOT ALPS_BUILD_LIBS_ONLY) ###################################################################### @@ -560,9 +487,9 @@ if (ALPS_BUILD_APPLICATIONS AND NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(applications) endif (ALPS_BUILD_APPLICATIONS AND NOT ALPS_BUILD_LIBS_ONLY) -if (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(tutorials) -endif (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +endif (ALPS_INCLUDE_TUTORIALS AND NOT ALPS_BUILD_LIBS_ONLY) ###################################################################### # developer tools diff --git a/applications/dmft/qmc/hybridization/hybmain.cpp b/applications/dmft/qmc/hybridization/hybmain.cpp index d02b7d5f2..ac063fda9 100644 --- a/applications/dmft/qmc/hybridization/hybmain.cpp +++ b/applications/dmft/qmc/hybridization/hybmain.cpp @@ -33,11 +33,11 @@ void master_final_tasks(const alps::results_type::type &results, int global_mpi_rank; #ifdef BUILD_PYTHON_MODULE -//compile it as a python module (requires boost::python library) -using namespace boost::python; +#include "dict_to_params.hpp" +namespace nb = nanobind; -void solve(boost::python::dict parms_){ - alps::parameters_type::type parms(parms_); +void solve(nb::dict const & parms_){ + alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else int main(int argc, char** argv){ @@ -134,12 +134,10 @@ void master_final_tasks(const alps::results_type::type &results, } #ifdef BUILD_PYTHON_MODULE -BOOST_PYTHON_MODULE(cthyb) -{ - def("solve",solve);//define python-callable run method -}; +NB_MODULE(cthyb, m) { + m.def("solve", solve); +} #endif - diff --git a/applications/dmft/qmc/interaction_expansion2/main.cpp b/applications/dmft/qmc/interaction_expansion2/main.cpp index da462c34b..74062ab7c 100644 --- a/applications/dmft/qmc/interaction_expansion2/main.cpp +++ b/applications/dmft/qmc/interaction_expansion2/main.cpp @@ -30,11 +30,11 @@ void compute_greens_functions(const alps::results_type::type parms(parms_); +void solve(nb::dict const & parms_){ + alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else int main(int argc, char** argv) @@ -110,9 +110,8 @@ int main(int argc, char** argv) } #ifdef BUILD_PYTHON_MODULE - BOOST_PYTHON_MODULE(ctint) - { - def("solve",solve);//define python-callable run method - }; + NB_MODULE(ctint, m) { + m.def("solve", solve); + } #endif diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt new file mode 100644 index 000000000..f7db44eec --- /dev/null +++ b/bindings/python/pyalps/CMakeLists.txt @@ -0,0 +1,139 @@ +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.18) +project(pyalps LANGUAGES CXX) + +option(PYALPS_BUILD_APPLICATIONS "Build optional ALPS application bindings" ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(ALPS REQUIRED CONFIG) +find_package(Python 3.9 REQUIRED COMPONENTS Interpreter Development.Module) + +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_VARIABLE _nanobind_cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) +list(PREPEND CMAKE_PREFIX_PATH "${_nanobind_cmake_dir}") +find_package(nanobind 2.10 CONFIG REQUIRED) + +get_filename_component(_repo_root "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +set(_bindings "${CMAKE_CURRENT_SOURCE_DIR}/cpp") + +link_directories(${ALPS_LIBRARY_DIRS}) +if(ALPS_HDF5_INCLUDE_DIR) + get_filename_component(_alps_hdf5_prefix "${ALPS_HDF5_INCLUDE_DIR}" DIRECTORY) + link_directories("${_alps_hdf5_prefix}/lib") +endif() + +# Legacy ALPSConfig.cmake serializes the HDF5 imported target name into +# ALPS_LIBRARIES. Outside the original build tree that target no longer +# exists, while the installed library keeps its conventional `hdf5` name. +set(_pyalps_link_libraries ${ALPS_LIBRARIES}) +list(TRANSFORM _pyalps_link_libraries REPLACE "^hdf5-shared$" "hdf5") +separate_arguments(_pyalps_link_options NATIVE_COMMAND "${ALPS_EXTRA_LINKER_FLAGS}") + +set(_pyalps_targets + pyalea_c + pymcdata_c + pytools_c + pyngsparams_c + pyngshdf5_c + pyngsbase_c + pyngsobservable_c + pyngsobservables_c + pyngsresult_c + pyngsresults_c + pyngsapi_c + pyngsrandom01_c + pyngsaccumulator_c) + +nanobind_add_module(pyalea_c NB_STATIC "${_bindings}/pyalea.cpp") +nanobind_add_module(pymcdata_c NB_STATIC "${_bindings}/pymcdata.cpp") +nanobind_add_module(pytools_c NB_STATIC "${_bindings}/pytools.cpp") +nanobind_add_module(pyngsparams_c NB_STATIC "${_bindings}/ngs/params.cpp") +nanobind_add_module(pyngshdf5_c NB_STATIC "${_bindings}/ngs/hdf5.cpp") +nanobind_add_module(pyngsbase_c NB_STATIC "${_bindings}/ngs/mcbase.cpp") +nanobind_add_module(pyngsobservable_c NB_STATIC "${_bindings}/ngs/observable.cpp") +nanobind_add_module(pyngsobservables_c NB_STATIC "${_bindings}/ngs/observables.cpp") +nanobind_add_module(pyngsresult_c NB_STATIC "${_bindings}/ngs/result.cpp") +nanobind_add_module(pyngsresults_c NB_STATIC "${_bindings}/ngs/results.cpp") +nanobind_add_module(pyngsapi_c NB_STATIC "${_bindings}/ngs/api.cpp") +nanobind_add_module(pyngsrandom01_c NB_STATIC "${_bindings}/ngs/random01.cpp") +nanobind_add_module(pyngsaccumulator_c NB_STATIC "${_bindings}/ngs/accumulator.cpp") + +if(PYALPS_BUILD_APPLICATIONS) + if(NOT EXISTS "${_repo_root}/tool/maxent.cpp") + message(FATAL_ERROR + "PYALPS_BUILD_APPLICATIONS requires an ALPS source checkout. " + "Configure with -DPYALPS_BUILD_APPLICATIONS=OFF for the core-only package.") + endif() + + set(_dmft "${_repo_root}/applications/dmft/qmc") + + nanobind_add_module(maxent_c NB_STATIC + "${_repo_root}/tool/maxent.cpp" + "${_repo_root}/tool/maxent_helper.cpp" + "${_repo_root}/tool/maxent_simulation.cpp" + "${_repo_root}/tool/maxent_parms.cpp") + + nanobind_add_module(cthyb NB_STATIC + "${_dmft}/hybridization/hybmain.cpp" + "${_dmft}/hybridization/hybsim.cpp" + "${_dmft}/hybridization/hyblocal.cpp" + "${_dmft}/hybridization/hybint.cpp" + "${_dmft}/hybridization/hybfun.cpp" + "${_dmft}/hybridization/hybretintfun.cpp" + "${_dmft}/hybridization/hybmatrix.cpp" + "${_dmft}/hybridization/hybmatrix_ft.cpp" + "${_dmft}/hybridization/hybconfig.cpp" + "${_dmft}/hybridization/hybupdates.cpp" + "${_dmft}/hybridization/hybevaluate.cpp" + "${_dmft}/hybridization/hybmeasurements.cpp") + + nanobind_add_module(ctint NB_STATIC + "${_dmft}/interaction_expansion2/main.cpp" + "${_dmft}/fouriertransform.C" + "${_dmft}/interaction_expansion2/auxiliary.cpp" + "${_dmft}/interaction_expansion2/observables.cpp" + "${_dmft}/interaction_expansion2/fastupdate.cpp" + "${_dmft}/interaction_expansion2/selfenergy.cpp" + "${_dmft}/interaction_expansion2/solver.cpp" + "${_dmft}/interaction_expansion2/io.cpp" + "${_dmft}/interaction_expansion2/splines.cpp" + "${_dmft}/interaction_expansion2/interaction_expansion.cpp" + "${_dmft}/interaction_expansion2/measurements.cpp" + "${_dmft}/interaction_expansion2/model.cpp") + + nanobind_add_module(dwa_c NB_STATIC "${_bindings}/apps/dwa.cpp") + + list(APPEND _pyalps_targets maxent_c cthyb ctint dwa_c) + foreach(_target IN ITEMS maxent_c cthyb ctint) + target_compile_definitions(${_target} PRIVATE BUILD_PYTHON_MODULE) + target_include_directories(${_target} PRIVATE "${_bindings}" "${_dmft}") + endforeach() + target_include_directories(dwa_c PRIVATE "${_repo_root}/applications/qmc/dwa") +endif() + +foreach(_target IN LISTS _pyalps_targets) + target_include_directories(${_target} PRIVATE + ${ALPS_INCLUDE_DIRS} + ${ALPS_EXTRA_INCLUDE_DIRS}) + target_link_libraries(${_target} PRIVATE ${_pyalps_link_libraries}) + target_link_options(${_target} PRIVATE ${_pyalps_link_options}) + set_target_properties(${_target} PROPERTIES + INSTALL_RPATH "${ALPS_LIBRARY_DIRS};${_alps_hdf5_prefix}/lib") +endforeach() + +set(_extension_dir "pyalps/_ext") +install(TARGETS ${_pyalps_targets} + LIBRARY DESTINATION "${_extension_dir}" + RUNTIME DESTINATION "${_extension_dir}") + +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/" + DESTINATION pyalps + FILES_MATCHING PATTERN "*.py") diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md new file mode 100644 index 000000000..e4c3507f3 --- /dev/null +++ b/bindings/python/pyalps/README.md @@ -0,0 +1,24 @@ +# pyalps + +Legacy-compatible Python bindings for ALPS, built as a standalone +`scikit-build-core` project using nanobind. The C++ ALPS library must be +built and installed separately; point `ALPS_DIR` at its `share/alps` +package directory when building this wheel. + +From the repository root: + +```sh +cmake -S . -B _build/alps -G Ninja \ + -DCMAKE_INSTALL_PREFIX="$PWD/_build/install" \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DALPS_BUILD_LIBS_ONLY=ON +cmake --build _build/alps --target install + +ALPS_DIR="$PWD/_build/install/share/alps" \ + CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" \ + python -m build --wheel +``` + +`PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, +DWA, CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake +configuration for a smaller core-only developer build. diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp new file mode 100644 index 000000000..d50c84e8d --- /dev/null +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -0,0 +1,35 @@ +// Copyright (C) 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#ifndef PYALPS_DICT_TO_PARAMS_HPP +#define PYALPS_DICT_TO_PARAMS_HPP +#include +#include +#include +#include +#include +#include +namespace pyalps { +namespace nb = nanobind; +inline alps::params params_from_dict(nb::dict const & values) { + alps::params result; + for (auto item : values) { + std::string key = nb::cast(nb::str(item.first)); + nb::handle value = item.second; + if (nb::isinstance(value)) + result[key] = nb::cast(value); + else if (nb::isinstance(value)) + result[key] = nb::cast(value); + else if (nb::isinstance(value)) + result[key] = nb::cast(value); + else if (nb::isinstance(value)) + result[key] = nb::cast(value); + else if (nb::isinstance(value) || nb::isinstance(value)) + result[key] = nb::cast>(value); + else + throw nb::type_error(("unsupported parameter type for '" + key + "'").c_str()); + } + return result; +} +} // namespace pyalps +#endif diff --git a/bindings/python/pyalps/cpp/ngs/accumulator.cpp b/bindings/python/pyalps/cpp/ngs/accumulator.cpp new file mode 100644 index 000000000..f3ca7f005 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/accumulator.cpp @@ -0,0 +1,123 @@ +// Copyright (C) 2010 - 2011 by Lukas Gamper +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace { +template +std::string print_value(T const & value) { + std::stringstream stream; + value.print(stream); + return stream.str(); +} +template +typename Accumulator::result_type make_result(Accumulator const & value) { + return typename Accumulator::result_type(value); +} +template +void bind_result_operators(nb::class_ & cls) { + cls + .def("__neg__", [](Result value) { value.negate(); return value; }) + .def("__iadd__", [](Result & self, Result const & other) -> Result & { self += other; return self; }, nb::rv_policy::reference_internal) + .def("__iadd__", [](Result & self, double value) -> Result & { self += value; return self; }, nb::rv_policy::reference_internal) + .def("__isub__", [](Result & self, Result const & other) -> Result & { self -= other; return self; }, nb::rv_policy::reference_internal) + .def("__isub__", [](Result & self, double value) -> Result & { self -= value; return self; }, nb::rv_policy::reference_internal) + .def("__imul__", [](Result & self, Result const & other) -> Result & { self *= other; return self; }, nb::rv_policy::reference_internal) + .def("__imul__", [](Result & self, double value) -> Result & { self *= value; return self; }, nb::rv_policy::reference_internal) + .def("__itruediv__", [](Result & self, Result const & other) -> Result & { self /= other; return self; }, nb::rv_policy::reference_internal) + .def("__itruediv__", [](Result & self, double value) -> Result & { self /= value; return self; }, nb::rv_policy::reference_internal) + .def("__add__", [](Result value, Result const & other) { value += other; return value; }, nb::is_operator()) + .def("__add__", [](Result value, double other) { value += other; return value; }, nb::is_operator()) + .def("__radd__", [](Result value, double other) { value += other; return value; }, nb::is_operator()) + .def("__sub__", [](Result value, Result const & other) { value -= other; return value; }, nb::is_operator()) + .def("__sub__", [](Result value, double other) { value -= other; return value; }, nb::is_operator()) + .def("__rsub__", [](Result value, double other) { value.negate(); value += other; return value; }, nb::is_operator()) + .def("__mul__", [](Result value, Result const & other) { value *= other; return value; }, nb::is_operator()) + .def("__mul__", [](Result value, double other) { value *= other; return value; }, nb::is_operator()) + .def("__rmul__", [](Result value, double other) { value *= other; return value; }, nb::is_operator()) + .def("__truediv__", [](Result value, Result const & other) { value /= other; return value; }, nb::is_operator()) + .def("__truediv__", [](Result value, double other) { value /= other; return value; }, nb::is_operator()) + .def("__rtruediv__", [](Result value, double other) { value.inverse(); value *= other; return value; }, nb::is_operator()) + .def("sin", [](Result value) { value.sin(); return value; }) + .def("cos", [](Result value) { value.cos(); return value; }) + .def("tan", [](Result value) { value.tan(); return value; }) + .def("sinh", [](Result value) { value.sinh(); return value; }) + .def("cosh", [](Result value) { value.cosh(); return value; }) + .def("tanh", [](Result value) { value.tanh(); return value; }) + .def("abs", [](Result value) { value.abs(); return value; }) + .def("sqrt", [](Result value) { value.sqrt(); return value; }) + .def("log", [](Result value) { value.log(); return value; }); +} +template +void bind_serializable(nb::class_ & cls) { + cls.def("__str__", &print_value) + .def("save", &T::save) + .def("load", &T::load) + .def("reset", &T::reset); +} +} // namespace +NB_MODULE(pyngsaccumulator_c, m) { + using namespace alps::accumulator::impl; + using count_accumulator = Accumulator>; + using count_result = count_accumulator::result_type; + nb::class_ count_acc(m, "count_accumulator"); + count_acc.def(nb::init<>()).def("__call__", [](count_accumulator & self, double value) { self(value); }) + .def("result", &make_result).def("count", &count_accumulator::count); + bind_serializable(count_acc); + nb::class_ count_res(m, "count_result"); + count_res.def(nb::init<>()).def("count", &count_result::count); + bind_serializable(count_res); bind_result_operators(count_res); + using mean_accumulator = Accumulator; + using mean_result = mean_accumulator::result_type; + nb::class_ mean_acc(m, "mean_accumulator"); + mean_acc.def(nb::init<>()).def("__call__", [](mean_accumulator & self, double value) { self(value); }) + .def("result", &make_result).def("count", &mean_accumulator::count) + .def("mean", &mean_accumulator::mean); + bind_serializable(mean_acc); + nb::class_ mean_res(m, "mean_result"); + mean_res.def(nb::init<>()).def("count", &mean_result::count).def("mean", &mean_result::mean); + bind_serializable(mean_res); bind_result_operators(mean_res); + using error_accumulator = Accumulator; + using error_result = error_accumulator::result_type; + nb::class_ error_acc(m, "error_accumulator"); + error_acc.def(nb::init<>()).def("__call__", [](error_accumulator & self, double value) { self(value); }) + .def("result", &make_result).def("count", &error_accumulator::count) + .def("mean", &error_accumulator::mean).def("error", &error_accumulator::error); + bind_serializable(error_acc); + nb::class_ error_res(m, "error_result"); + error_res.def(nb::init<>()).def("count", &error_result::count).def("mean", &error_result::mean) + .def("error", &error_result::error); + bind_serializable(error_res); bind_result_operators(error_res); + using binning_accumulator = Accumulator; + using binning_result = binning_accumulator::result_type; + nb::class_ binning_acc(m, "binning_analysis_accumulator"); + binning_acc.def(nb::init<>()).def("__call__", [](binning_accumulator & self, double value) { self(value); }) + .def("result", &make_result).def("count", &binning_accumulator::count) + .def("mean", &binning_accumulator::mean).def("error", &binning_accumulator::error); + bind_serializable(binning_acc); + nb::class_ binning_res(m, "binning_analysis_result"); + binning_res.def(nb::init<>()).def("count", &binning_result::count).def("mean", &binning_result::mean) + .def("error", &binning_result::error); + bind_serializable(binning_res); bind_result_operators(binning_res); + using maxbin_accumulator = Accumulator; + using maxbin_result = maxbin_accumulator::result_type; + nb::class_ maxbin_acc(m, "max_num_binning_accumulator"); + maxbin_acc.def(nb::init<>()).def("__call__", [](maxbin_accumulator & self, double value) { self(value); }) + .def("result", &make_result).def("count", &maxbin_accumulator::count) + .def("mean", &maxbin_accumulator::mean).def("error", &maxbin_accumulator::error); + bind_serializable(maxbin_acc); + nb::class_ maxbin_res(m, "max_num_binning_result"); + maxbin_res.def(nb::init<>()).def("count", &maxbin_result::count).def("mean", &maxbin_result::mean) + .def("error", &maxbin_result::error); + bind_serializable(maxbin_res); bind_result_operators(maxbin_res); +} diff --git a/bindings/python/pyalps/cpp/ngs/api.cpp b/bindings/python/pyalps/cpp/ngs/api.cpp new file mode 100644 index 000000000..09a90dc4b --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/api.cpp @@ -0,0 +1,32 @@ +// Copyright (C) 2010 - 2011 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +// umbrella retired in Phase 4 Slice 6 of the NGS +// retirement (ngs-retirement-scoping.md). This binding's surviving +// surface (`saveResults`) only needs `alps::mcresults`, +// `alps::params`, and `alps::hdf5::archive` — pull the narrow +// headers directly. +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace alps { + namespace detail { + void save_results_export(mcresults const & res, params const & par, alps::hdf5::archive & ar, std::string const & path) { + ar["/parameters"] << par; + if (res.size()) + ar[path] << res; + } + } +} +NB_MODULE(pyngsapi_c, m) { + m.def("collectResults", [](alps::mcbase const & sim) { + return alps::collect_results(sim); + }); + m.def("saveResults", &alps::detail::save_results_export); +} diff --git a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp new file mode 100644 index 000000000..3023d4bad --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp @@ -0,0 +1,130 @@ +// Copyright (C) 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +/// Header-only template: dispatches a nb::object to a visitor based +/// on the object's Python type name. For numpy arrays the buffer +/// protocol (PEP 3118) provides the raw data + shape + format string; +/// for numpy scalars nb::cast handles the conversion through the +/// scalar's __int__/__float__/__complex__ methods. No dependence on +/// . +#ifndef PYALPS_NGS_EXTRACT_FROM_PYOBJECT_HPP +#define PYALPS_NGS_EXTRACT_FROM_PYOBJECT_HPP + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + namespace alps { + namespace detail { + namespace nb_ = nanobind; + // True iff the ndarray view is C-contiguous: strides match + // the canonical row-major layout (rightmost stride = + // itemsize, each leftward stride = previous * shape[i+1]). + // Zero-rank scalars are trivially contiguous. + template + inline bool ndarray_is_c_contiguous(Arr const & arr) { + if (arr.ndim() == 0) return true; + int64_t expected = 1; + for (int64_t i = arr.ndim() - 1; i >= 0; --i) { + if (arr.shape(i) == 0) return true; // empty array + if (arr.stride(i) != expected) return false; + expected *= arr.shape(i); + } + return true; + } + /// Dispatches `data` to `visitor` based on Python's type + /// name. Visitor must be callable with bool / int / long / + /// double / std::complex / std::string / + /// nb::list / nb::dict, plus the two-arg numpy form + /// `visitor(T const*, std::vector)` for each + /// supported native numpy element type. + template void extract_from_pyobject_py11(T & visitor, nb_::handle data) { + std::string dtype = data.ptr()->ob_type->tp_name; + if (dtype == "bool") visitor(nb_::cast(data)); + else if (dtype == "int") visitor(nb_::cast(data)); + else if (dtype == "long") visitor(nb_::cast(data)); + else if (dtype == "float") visitor(nb_::cast(data)); + else if (dtype == "complex") visitor(nb_::cast>(data)); + else if (dtype == "str") visitor(nb_::cast(data)); + else if (dtype == "list") visitor(nb_::borrow(data)); + else if (dtype == "tuple") { + // materialise the tuple as a list so the visitor only + // needs one sequence overload. + nb_::list as_list = nb_::steal( + PySequence_List(data.ptr())); + visitor(as_list); + } + else if (dtype == "dict") visitor(nb_::borrow(data)); + // numpy scalars: extract through the scalar's own + // __int__/__float__/__complex__ — no numpy C macros. + else if (dtype == "numpy.str_" || dtype == "numpy.str") + visitor(std::string(nb_::cast(nb_::str(data.attr("__str__")())))); + else if (dtype == "numpy.bool_" || dtype == "numpy.bool") + visitor(nb_::cast(data)); + else if (dtype == "numpy.int8") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.int16") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.int32") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.int64") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.uint8") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.uint16") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.uint32") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.uint64") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.float32") visitor(static_cast(nb_::cast(data))); + else if (dtype == "numpy.float64") visitor(nb_::cast(data)); + else if (dtype == "numpy.complex64") + visitor(std::complex( + nb_::cast(data.attr("real").attr("__float__")()) + , nb_::cast(data.attr("imag").attr("__float__")()) + )); + else if (dtype == "numpy.complex128") + visitor(nb_::cast>(data)); + else if (dtype == "numpy.ndarray") { + // Raw buffer access via nb::ndarray, with a strict + // dtype match — nb::cast>(arr) of a + // mismatched-dtype array silently coerces (e.g. + // int → bool yields all-true), so we inspect + // .dtype() ourselves and pick the matching arm. + // We require C-contiguity; the typical save path + // is bulk contiguous data and silently copying + // behind the user's back was the old + // PyArray_GETCONTIGUOUS behaviour we don't want + // to inherit. + auto arr_any = nb_::cast>(data); + std::vector sizes; + sizes.reserve(arr_any.ndim()); + for (std::size_t i = 0; i < arr_any.ndim(); ++i) + sizes.push_back(static_cast(arr_any.shape(i))); + auto dt = arr_any.dtype(); + #define DISPATCH_DTYPE(T) \ + if (dt == nb_::dtype()) \ + return visitor(static_cast(arr_any.data()), sizes); + DISPATCH_DTYPE(bool) + DISPATCH_DTYPE(signed char) + DISPATCH_DTYPE(unsigned char) + DISPATCH_DTYPE(short) + DISPATCH_DTYPE(unsigned short) + DISPATCH_DTYPE(int) + DISPATCH_DTYPE(unsigned) + DISPATCH_DTYPE(long) + DISPATCH_DTYPE(unsigned long) + DISPATCH_DTYPE(long long) + DISPATCH_DTYPE(unsigned long long) + DISPATCH_DTYPE(float) + DISPATCH_DTYPE(double) + DISPATCH_DTYPE(std::complex) + DISPATCH_DTYPE(std::complex) + #undef DISPATCH_DTYPE + throw std::runtime_error( + "Unknown numpy element dtype at save site" + ALPS_STACKTRACE); + } else + throw std::runtime_error("Unsupported type: " + dtype + ALPS_STACKTRACE); + } + } // namespace detail + } // namespace alps +#endif // PYALPS_NGS_EXTRACT_FROM_PYOBJECT_HPP diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp new file mode 100644 index 000000000..05eeefe92 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -0,0 +1,317 @@ +// Copyright (C) 2010 - 2012 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +// Save path: extract_from_pyobject_py11 dispatches a nb::handle to a +// visitor that writes a concrete C++ value. Load path: dispatches on +// the archive's inspected type and reads into a concrete C++ type +// before wrapping it back as a nb::object. +// +// Exception translation: pyalps/hdf5.py creates ArchiveError etc. and +// calls register_archive_exception_type(id, type); the translators +// below fire PyErr_SetString against whichever Python type was handed +// in. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "extract_from_pyobject.hpp" +#include "../numpy_compat.hpp" +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace alps { + namespace detail { + // Save-side visitor: receives a concrete C++ value (or a + // nb::list / nb::dict) from extract_from_pyobject_py11 and + // writes it to the archive at `path`. + struct hdf5_save_py11_visitor { + alps::hdf5::archive & ar; + std::string path; + template + void operator()(U const & v) const { + ar[path] << v; + } + template + void operator()(U const * ptr, std::vector const & sizes) const { + // Use make_pvp(path, ptr, size-vector) to preserve the + // dimensional shape — a plain vector flatten would + // round-trip the data but lose the rank. + ar << alps::make_pvp(path, ptr, sizes); + } + void operator()(nb::list const & l) const { + // Order: flat numeric first, then nested numeric, then + // strings. Heterogeneous / deeply-nested / mixed-type + // lists fall through to the descent branch below which + // stores each entry under a numeric child path. + try { ar[path] << nb::cast>(l); return; } + catch (nb::cast_error const &) {} + try { ar[path] << nb::cast>(l); return; } + catch (nb::cast_error const &) {} + try { ar[path] << nb::cast>>(l); return; } + catch (nb::cast_error const &) {} + try { ar[path] << nb::cast>>(l); return; } + catch (nb::cast_error const &) {} + try { ar[path] << nb::cast>>(l); return; } + catch (nb::cast_error const &) {} + try { ar[path] << nb::cast>(l); return; } + catch (nb::cast_error const &) {} + // Inhomogeneous — recurse per-element into + // /, letting each entry be stored as its + // own native type. + ar.create_group(path); + Py_ssize_t i = 0; + for (auto item : l) { + std::string child = path + "/" + std::to_string(static_cast(i++)); + hdf5_save_py11_visitor child_visitor{ar, child}; + extract_from_pyobject_py11(child_visitor, item); + } + } + void operator()(nb::dict const & d) const { + // Store a dict as a group with one child per key. Keys + // are stringified (HDF5 paths are strings), values go + // through the same save dispatch recursively. + ar.create_group(path); + for (auto item : d) { + std::string key = nb::cast(nb::str(item.first)); + std::string child = path + "/" + key; + hdf5_save_py11_visitor child_visitor{ar, child}; + extract_from_pyobject_py11(child_visitor, item.second); + } + } + }; + std::string python_hdf5_get_filename(alps::hdf5::archive & ar) { + return ar.get_filename(); + } + void python_hdf5_save(alps::hdf5::archive & ar, + std::string const & path, + nb::handle data) { + hdf5_save_py11_visitor visitor{ar, path}; + extract_from_pyobject_py11(visitor, data); + } + // Helper: load a multi-dim HDF5 dataset of element type T + // into a flat std::vector, then wrap as a numpy array with + // the original shape so that Python sees a 2-D np.array for + // rank-2 writes etc. Preserves the dimensionality encoded on + // the save path (alps::make_pvp(path, ptr, size-vector)). + template + nb::object load_nd_array(alps::hdf5::archive & ar, + std::string const & path, + std::vector const & shape) { + std::size_t total = 1; + for (auto s : shape) total *= s; + std::vector flat(total); + if (shape.size() <= 1) { + // vector overload works directly. + ar[path] >> flat; + } else { + // make_pvp with explicit size-vector to read a + // multi-dim dataset into a flat buffer. + ar >> alps::make_pvp(path, flat.data(), shape); + } + return alps::python::make_numpy_array(flat.data(), shape); + } + nb::object python_hdf5_load_impl(alps::hdf5::archive & ar, + std::string const & path); + nb::object python_hdf5_load(alps::hdf5::archive & ar, + std::string const & path) { + return python_hdf5_load_impl(ar, path); + } + nb::object python_hdf5_load_impl(alps::hdf5::archive & ar, + std::string const & path) { + // Groups (not datasets) get loaded recursively. Children + // whose names are consecutive decimal integers starting at 0 + // are recovered as a Python list (preserving round-trip for + // list-saved-as-group); otherwise a dict. + if (ar.is_group(path)) { + auto children = ar.list_children(path); + bool list_shaped = true; + for (std::size_t i = 0; list_shaped && i < children.size(); ++i) { + if (children[i] != std::to_string(i)) + list_shaped = false; + } + if (list_shaped) { + nb::list result; + for (auto const & child : children) + result.append( + python_hdf5_load_impl(ar, path + "/" + child)); + return nb::object(std::move(result)); + } else { + nb::dict result; + for (auto const & child : children) + result[nb::str(child.c_str())] = + python_hdf5_load_impl(ar, path + "/" + child); + return nb::object(std::move(result)); + } + } + // Complex values have a quirky HDF5 representation: a + // single complex is stored as rank-1 dims=[2] (real,imag) + // and a 2x2 array of complex as rank-3 dims=[2,2,2]. So + // is_scalar returns false for a scalar complex — branch + // on is_complex first and use the rank minus 1 (stripping + // the trailing complex-pair dim) to tell scalar from + // array. + if (ar.is_complex(path)) { + auto ext = ar.extent(path); + if (ext.size() == 1) { + std::complex v; ar[path] >> v; return nb::cast(v); + } + std::vector shape(ext.begin(), ext.end() - 1); + return load_nd_array>(ar, path, shape); + } + // Convenience macros for the scalar path: check each + // candidate integer width in turn (numpy's default int is + // platform-dependent — int64 on macOS/Linux, int32 on + // Windows — so we can't rely on just `int` matching). + #define TRY_SCALAR(T) \ + if (ar.is_datatype(path)) { T v; ar[path] >> v; return nb::cast(v); } + if (ar.is_scalar(path)) { + TRY_SCALAR(std::string) + TRY_SCALAR(double) + TRY_SCALAR(float) + TRY_SCALAR(bool) + TRY_SCALAR(std::int64_t) + TRY_SCALAR(std::int32_t) + TRY_SCALAR(std::int16_t) + TRY_SCALAR(std::int8_t) + TRY_SCALAR(std::uint64_t) + TRY_SCALAR(std::uint32_t) + TRY_SCALAR(std::uint16_t) + TRY_SCALAR(std::uint8_t) + throw std::runtime_error( + "Unknown HDF5 scalar type at " + path + ALPS_STACKTRACE); + } else { + // String datasets don't map to nb::ndarray the way + // numeric types do; keep the flat vector path. + if (ar.is_datatype(path)) { + std::vector v; ar[path] >> v; return nb::cast(v); + } + auto shape = ar.extent(path); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + if (ar.is_datatype(path)) return load_nd_array(ar, path, shape); + throw std::runtime_error( + "Unknown HDF5 vector type at " + path + ALPS_STACKTRACE); + } + #undef TRY_SCALAR + } + nb::list python_hdf5_extent(alps::hdf5::archive & ar, + std::string const & path) { + nb::list result; + std::vector ext = ar.extent(path); + if (ar.is_complex(path)) { + if (ext.size() > 1) + ext.pop_back(); + else + ext.back() = 1; + } + for (auto const & s : ext) + result.append(s); + return result; + } + // Python exception types registered by pyalps.hdf5 at import + // time. Translators below fire PyErr_SetString against these + // pre-registered PyObject*'s so the Python side sees its own + // subclasses (ArchiveError / ArchiveNotFound / ...). + std::array exception_type = {}; + #define TRANSLATE_CPP_ERROR_TO_PYTHON(T, ID) \ + static void translate_ ## T (hdf5:: T const & e) { \ + std::string message = \ + std::string(e.what()).substr( \ + 0, std::string(e.what()).find_first_of('\n')); \ + PyErr_SetString(exception_type[ID] ? exception_type[ID] \ + : PyExc_RuntimeError, \ + message.c_str()); \ + } + TRANSLATE_CPP_ERROR_TO_PYTHON(archive_error, 0) + TRANSLATE_CPP_ERROR_TO_PYTHON(archive_not_found, 1) + TRANSLATE_CPP_ERROR_TO_PYTHON(archive_closed, 2) + TRANSLATE_CPP_ERROR_TO_PYTHON(invalid_path, 3) + TRANSLATE_CPP_ERROR_TO_PYTHON(path_not_found, 4) + TRANSLATE_CPP_ERROR_TO_PYTHON(wrong_type, 5) + #undef TRANSLATE_CPP_ERROR_TO_PYTHON + void register_exception_type(int id, nb::object type) { + if (id < 0 || id >= static_cast(exception_type.size())) + throw std::out_of_range( + "register_archive_exception_type: id out of range"); + // Py_INCREF the incoming type so it survives past this call + // (we're keeping a raw PyObject* in a static array). + Py_INCREF(type.ptr()); + exception_type[id] = type.ptr(); + } + } +} +NB_MODULE(pyngshdf5_c, m) { + // Install the six C++→Python exception translators. Each calls the + // matching translate_* above, which forwards to whichever Python + // class was registered via register_archive_exception_type. If + // pyalps/hdf5.py hasn't run yet, the translator falls back to + // RuntimeError so the module is safely loadable on its own. + nb::register_exception_translator( + [](const std::exception_ptr &p, void * /*payload*/) { + try { std::rethrow_exception(p); } + catch (alps::hdf5::archive_not_found const & e) { + alps::detail::translate_archive_not_found(e); + } catch (alps::hdf5::archive_closed const & e) { + alps::detail::translate_archive_closed(e); + } catch (alps::hdf5::invalid_path const & e) { + alps::detail::translate_invalid_path(e); + } catch (alps::hdf5::path_not_found const & e) { + alps::detail::translate_path_not_found(e); + } catch (alps::hdf5::wrong_type const & e) { + alps::detail::translate_wrong_type(e); + } catch (alps::hdf5::archive_error const & e) { + // Base class — must be caught LAST since the specialized + // types above inherit from it. + alps::detail::translate_archive_error(e); + } + }); + m.def("register_archive_exception_type", + &alps::detail::register_exception_type); + nb::class_(m, "hdf5_archive_impl") + .def(nb::init()) + .def("__deepcopy__", + // copy.deepcopy() hands us (self, memo); memo unused. + [](alps::hdf5::archive const & self, nb::handle /*memo*/) { + return alps::hdf5::archive(self); + }) + .def_prop_ro("filename", &alps::detail::python_hdf5_get_filename) + .def_prop_ro("context", &alps::hdf5::archive::get_context) + .def_prop_ro("is_open", &alps::hdf5::archive::is_open) + .def("set_context", &alps::hdf5::archive::set_context) + .def("is_group", &alps::hdf5::archive::is_group) + .def("is_data", &alps::hdf5::archive::is_data) + .def("is_attribute", &alps::hdf5::archive::is_attribute) + .def("close", &alps::hdf5::archive::close) + .def("extent", &alps::detail::python_hdf5_extent) + .def("dimensions", &alps::hdf5::archive::dimensions) + .def("is_scalar", &alps::hdf5::archive::is_scalar) + .def("is_complex", &alps::hdf5::archive::is_complex) + .def("is_null", &alps::hdf5::archive::is_null) + .def("list_children", &alps::hdf5::archive::list_children) + .def("list_attributes", &alps::hdf5::archive::list_attributes) + .def("__setitem__", &alps::detail::python_hdf5_save) + .def("__getitem__", &alps::detail::python_hdf5_load) + .def("create_group", &alps::hdf5::archive::create_group) + .def("delete_data", &alps::hdf5::archive::delete_data) + .def("delete_group", &alps::hdf5::archive::delete_group) + .def("delete_attribute",&alps::hdf5::archive::delete_attribute); +} diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp new file mode 100644 index 000000000..0c4b704c7 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -0,0 +1,173 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * ALPS Project: Algorithms and Libraries for Physics Simulations * + * * + * ALPS Libraries * + * * + * Copyright (C) 2010 - 2011 by Lukas Gamper * + * Matthias Troyer * + * 2026 by the ALPS collaboration * + * * + * Permission is hereby granted, free of charge, to any person obtaining * + * a copy of this software and associated documentation files (the "Software"), * + * to deal in the Software without restriction, including without limitation * + * the rights to use, copy, modify, merge, publish, distribute, sublicense, * + * and/or sell copies of the Software, and to permit persons to whom the * + * Software is furnished to do so, subject to the following conditions: * + * * + * The above copyright notice and this permission notice shall be included * + * in all copies or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * + * DEALINGS IN THE SOFTWARE. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// pyngsbase_c — nanobind port. +// +// Trampoline (PyMCBase) forwards the three pure-virtual mcbase methods +// (update / measure / fraction_completed) back into the Python subclass +// through nanobind's trampoline support. The old wrapper +// pattern becomes a standard trampoline-plus-alias pair. +// +// Params ingestion: the public alps::mcbase ctor wants an alps::params. +// libalps still declares a params(boost::python::dict) ctor in its +// header, but we don't want to drag boost::python through the +// nanobind bindings. Instead, we convert nb::dict → alps::params at the +// binding boundary by iterating and setitem-ing concrete C++ values +// (int/float/bool/str/list). That sidesteps the cross-registry issue +// and keeps the libalps ABI untouched. +#define PY_ARRAY_UNIQUE_SYMBOL pyngsbase_PyArrayHandle +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +#ifdef ALPS_HAVE_MPI + #include +#endif +#include +#include +#include +#include +#include +namespace alps { + namespace detail { + // Convert a Python dict into an alps::params, extracting concrete + // C++ values for each entry. This mirrors what the libalps + // params(boost::python::dict) ctor does, but without routing the + // nb::object through the boost::python::object variant alternative + // — everything stays within the nanobind type registry. + inline alps::params py_dict_to_params(nb::dict const & d) { + alps::params p; + for (auto item : d) { + std::string k = nb::cast(nb::str(item.first)); + nb::handle v = item.second; + if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v) || nb::isinstance(v)) + p[k] = nb::cast>(v); + else + throw nb::type_error(( + "unsupported type for key '" + k + + "' in params dict (expected bool/int/float/str/list)").c_str()); + } + return p; + } + } +} +namespace alps { + // Trampoline: holds Python overrides for pure-virtuals. The + // protected mcbase members (random / parameters / measurements) + // are accessed via lambdas in the binding below, which friend-in + // through PyMCBase (a protected member is visible to a derived + // class's own member functions / friends). + class PyMCBase : public mcbase { + public: + NB_TRAMPOLINE(mcbase, 3); + #ifdef ALPS_HAVE_MPI + PyMCBase(nb::dict const & arg, + std::size_t seed_offset = 42, + boost::mpi::communicator const & /*comm*/ = boost::mpi::communicator()) + : mcbase(alps::detail::py_dict_to_params(arg), seed_offset) + {} + #else + PyMCBase(nb::dict const & arg, std::size_t seed_offset = 42) + : mcbase(alps::detail::py_dict_to_params(arg), seed_offset) + {} + #endif + void update() override { + NB_OVERRIDE_PURE(update); + } + void measure() override { + NB_OVERRIDE_PURE(measure); + } + double fraction_completed() const override { + NB_OVERRIDE_PURE(fraction_completed); + } + // Accessors for protected mcbase members. Called from the + // binding lambdas below (they friend-in through PyMCBase). + alps::random01 & get_random() { return random; } + mcbase::parameters_type & get_parameters() { return parameters; } + alps::mcobservables & get_measurements() { return measurements; } + // mcbase::run takes a std::function; wrap a Python + // callable so the stop_callback can be driven from Python. + bool run_py(nb::object stop_callback) { + return mcbase::run([stop_callback]() -> bool { + nb::gil_scoped_acquire gil; + return nb::cast(stop_callback()); + }); + } + }; +} +NB_MODULE(pyngsbase_c, m) { + nb::class_(m, "_mcbase", nb::never_destruct()); + nb::class_(m, "mcbase") + // Always expose the (dict, seed_offset) form from Python. When + // ALPS_HAVE_MPI is on we'd *like* to offer an optional + // communicator too, but boost::mpi::communicator is not a + // nanobind-registered type so nb::arg(..).default_value() can't + // materialise it. MPI simulations that actually need to hand + // Python a communicator should do so from C++ using the + // extended trampoline ctor directly. + .def(nb::init(), + nb::arg("dict"), + nb::arg("seed_offset") = 42) + .def_prop_ro( + "random", + [](alps::PyMCBase & self) -> alps::random01 & { return self.get_random(); }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "parameters", + [](alps::PyMCBase & self) -> alps::mcbase::parameters_type & { return self.get_parameters(); }, + nb::rv_policy::reference_internal) + .def_prop_ro( + "measurements", + [](alps::PyMCBase & self) -> alps::mcobservables & { return self.get_measurements(); }, + nb::rv_policy::reference_internal) + .def("run", + [](alps::PyMCBase & self, nb::object cb) { return self.run_py(std::move(cb)); }) + // Pure-virtual methods: bound on the base class; the trampoline's + // The trampoline forwards the call into the Python subclass. + .def("update", &alps::mcbase::update) + .def("measure", &alps::mcbase::measure) + .def("fraction_completed", &alps::mcbase::fraction_completed) + .def("save", static_cast( + &alps::mcbase::save)) + .def("load", static_cast( + &alps::mcbase::load)); +} diff --git a/bindings/python/pyalps/cpp/ngs/observable.cpp b/bindings/python/pyalps/cpp/ngs/observable.cpp new file mode 100644 index 000000000..dde673f21 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/observable.cpp @@ -0,0 +1,81 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * ALPS Project: Algorithms and Libraries for Physics Simulations * + * * + * ALPS Libraries * + * * + * Copyright (C) 2010 - 2011 by Lukas Gamper * + * Matthias Troyer * + * 2026 by the ALPS collaboration * + * * + * Permission is hereby granted, free of charge, to any person obtaining * + * a copy of this software and associated documentation files (the "Software"), * + * to deal in the Software without restriction, including without limitation * + * the rights to use, copy, modify, merge, publish, distribute, sublicense, * + * and/or sell copies of the Software, and to permit persons to whom the * + * Software is furnished to do so, subject to the following conditions: * + * * + * The above copyright notice and this permission notice shall be included * + * in all copies or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * + * DEALINGS IN THE SOFTWARE. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// pyngsobservable_c — nanobind port. +#define PY_ARRAY_UNIQUE_SYMBOL pyngsobservable_PyArrayHandle +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +#include +#include +#include +#include +#include +namespace alps { + namespace detail { + void observable_append(alps::mcobservable & self, nb::object const & data) { + if (nb::isinstance(data) || nb::isinstance(data)) { + self << nb::cast(data); + return; + } + try { + auto values = nb::cast>(data); + self << std::valarray(values.data(), values.size()); + } catch (nb::cast_error const &) { + throw nb::type_error("observable samples must be numeric scalars or contiguous float64 arrays"); + } + } + void observable_load(alps::mcobservable & self, alps::hdf5::archive & ar, std::string const & path) { + std::string current = ar.get_context(); + ar.set_context(path); + self.load(ar); + ar.set_context(current); + } + alps::mcobservable create_RealObservable_export(std::string name) { + return alps::mcobservable(std::make_shared(name).get()); + } + alps::mcobservable create_RealVectorObservable_export(std::string name) { + return alps::mcobservable(std::make_shared(name).get()); + } + } +} +NB_MODULE(pyngsobservable_c, m) { + m.def("createRealObservable", &alps::detail::create_RealObservable_export); + m.def("createRealVectorObservable", &alps::detail::create_RealVectorObservable_export); + nb::class_(m, "observable") + .def("append", &alps::detail::observable_append) + .def("merge", &alps::mcobservable::merge) + .def("save", &alps::mcobservable::save) + .def("load", &alps::detail::observable_load) + .def("addToObservable", &alps::detail::observable_load); +} diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp new file mode 100644 index 000000000..7b121fe64 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -0,0 +1,108 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * ALPS Project: Algorithms and Libraries for Physics Simulations * + * * + * ALPS Libraries * + * * + * Copyright (C) 2010 - 2011 by Lukas Gamper * + * Matthias Troyer * + * 2026 by the ALPS collaboration * + * * + * Permission is hereby granted, free of charge, to any person obtaining * + * a copy of this software and associated documentation files (the "Software"), * + * to deal in the Software without restriction, including without limitation * + * the rights to use, copy, modify, merge, publish, distribute, sublicense, * + * and/or sell copies of the Software, and to permit persons to whom the * + * Software is furnished to do so, subject to the following conditions: * + * * + * The above copyright notice and this permission notice shall be included * + * in all copies or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * + * DEALINGS IN THE SOFTWARE. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +// pyngsobservables_c — nanobind port. +// +// Unlike mcresults, alps::mcobservables doesn't override erase() so we +// can stand on a straight nb::class_<> with hand-written map methods +// that match the map_indexing_suite surface area. We keep it explicit +// (rather than nb::bind_map) because the class also carries non-map +// methods (reset/save/load/__lshift__/create*) that need to live on +// the same binding, and mixing bind_map with extra defs is noisy. +#define PY_ARRAY_UNIQUE_SYMBOL pyngsobservables_PyArrayHandle +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +#include +#include +namespace { +void mcobservables_load(alps::mcobservables & self, alps::hdf5::archive & ar, std::string const & path) { + std::string current = ar.get_context(); + ar.set_context(path); + self.load(ar); + ar.set_context(current); +} +void createRealObservable(alps::mcobservables & self, std::string const & name, std::uint32_t binnum) { + self << alps::ngs::RealObservable(name, binnum); +} +void createRealVectorObservable(alps::mcobservables & self, std::string const & name, std::uint32_t binnum) { + self << alps::ngs::RealVectorObservable(name, binnum); +} +void addObservable(alps::mcobservables & self, nb::object const & obj) { + // Mirror boost::python::call_method(obj, "addToObservables", ref(self)): + // bounce the call back into Python, passing `self` by reference. + obj.attr("addToObservables")(nb::cast(&self, nb::rv_policy::reference)); +} +} // namespace +NB_MODULE(pyngsobservables_c, m) { + nb::class_(m, "observables") + .def(nb::init<>()) + .def("__len__", [](alps::mcobservables const & self) { return self.size(); }) + .def("__contains__", [](alps::mcobservables const & self, std::string const & k) { + return self.has(k); + }) + .def("__getitem__", [](alps::mcobservables & self, std::string const & k) -> alps::mcobservable & { + if (!self.has(k)) + throw nb::key_error(k.c_str()); + return self[k]; + }, + nb::rv_policy::reference_internal) + .def("__setitem__", [](alps::mcobservables & self, std::string const & k, alps::mcobservable const & v) { + self.insert(k, v); + }) + .def("__iter__", [](alps::mcobservables & self) { + return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("keys", [](alps::mcobservables & self) { + return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("values", [](alps::mcobservables & self) { + return nb::make_value_iterator(nb::type(), "value_iterator", self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("items", [](alps::mcobservables & self) { + return nb::make_iterator(nb::type(), "item_iterator", self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("reset", &alps::mcobservables::reset, nb::arg("equilibrated") = false) + .def("save", &alps::mcobservables::save) + .def("load", &mcobservables_load) + .def("__lshift__", &addObservable) + .def("createRealObservable", &createRealObservable, + nb::arg("name"), nb::arg("binnum") = 0) + .def("createRealVectorObservable", &createRealVectorObservable, + nb::arg("name"), nb::arg("binnum") = 0); +} diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp new file mode 100644 index 000000000..f6d9c8957 --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -0,0 +1,160 @@ +// Copyright (C) 2010 - 2011 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace { +// Convert a Python dict into an alps::params. Same shape as the +// helper in mcbase.cpp but kept local to params.cpp so a change to +// the dispatch (e.g. adding complex support) can stay in one place +// alongside the other setitem logic. +alps::params py_dict_to_params(nb::dict const & d); +// Walk the paramvalue variant and wrap each native alternative as a +// nb::object. Called from __getitem__. +struct paramvalue_to_py_visitor : boost::static_visitor { + template + nb::object operator()(T const & value) const { + return nb::cast(value); + } +}; +nb::object paramvalue_to_py(alps::detail::paramvalue const & pv) { + return boost::apply_visitor( + paramvalue_to_py_visitor(), + static_cast(pv)); +} +// Deposit a native C++ value from a Python object into the paramvalue +// via paramproxy's templated operator=. +void params_setitem(alps::params & self, nb::object const & key_obj, nb::object const & value) { + std::string key = nb::cast(nb::str(key_obj)); + if (nb::isinstance(value)) + self[key] = nb::cast(value); + else if (nb::isinstance(value)) + self[key] = nb::cast(value); + else if (nb::isinstance(value)) + self[key] = nb::cast(value); + else if (nb::isinstance(value)) + self[key] = nb::cast(value); + else if (nb::isinstance(value) || nb::isinstance(value)) { + // Heuristic: try doubles first, strings as fallback. + try { + self[key] = nb::cast>(value); + } catch (nb::cast_error &) { + self[key] = nb::cast>(value); + } + } else { + throw nb::type_error("unsupported value type for params[]"); + } +} +nb::object params_getitem(alps::params & self, nb::object const & key_obj) { + std::string key = nb::cast(nb::str(key_obj)); + if (!self.defined(key)) + return nb::none(); + // params doesn't expose the underlying map directly, but + // paramiterator yields (key, paramvalue) pairs; walk it to find the + // entry and hand the variant to paramvalue_to_py. + for (auto it = self.begin(); it != self.end(); ++it) + if (it->first == key) + return paramvalue_to_py(it->second); + return nb::none(); // defensive — defined()==true should guarantee a hit +} +void params_delitem(alps::params & self, nb::object const & key_obj) { + self.erase(nb::cast(nb::str(key_obj))); +} +bool params_contains(alps::params & self, nb::object const & key_obj) { + return self.defined(nb::cast(nb::str(key_obj))); +} +nb::object value_or_default(alps::params & self, nb::object const & key, nb::object const & dflt) { + return params_contains(self, key) ? params_getitem(self, key) : dflt; +} +void params_load(alps::params & self, alps::hdf5::archive & ar, std::string const & path) { + std::string current = ar.get_context(); + ar.set_context(path); + self.load(ar); + ar.set_context(current); +} +std::string params_print(alps::params & self) { + std::stringstream ss; + ss << self; + return ss.str(); +} +// deepcopy support — nanobind passes (self, memo); memo unused. +alps::params params_deepcopy(alps::params const & self, nb::handle /*memo*/) { + return alps::params(self); +} +// Materialise an alps::params from a Python dict. Re-uses the same +// type dispatch as params_setitem so a round-tripped dict-built +// params contains exactly the same variant alternatives. +alps::params py_dict_to_params(nb::dict const & d) { + alps::params p; + for (auto item : d) { + std::string k = nb::cast(nb::str(item.first)); + nb::handle v = item.second; + if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v)) + p[k] = nb::cast(v); + else if (nb::isinstance(v) || nb::isinstance(v)) { + try { p[k] = nb::cast>(v); } + catch (nb::cast_error &) { + p[k] = nb::cast>(v); + } + } else { + throw nb::type_error( + ("unsupported value type for params key '" + k + "'").c_str()); + } + } + return p; +} +} // namespace +NB_MODULE(pyngsparams_c, m) { + nb::class_(m, "params") + .def(nb::init<>()) + .def("__init__", + [](alps::params * self, nb::dict const & d) { + new (self) alps::params(py_dict_to_params(d)); + }, + nb::arg("dict")) + .def(nb::init(), + nb::arg("archive"), + nb::arg("path") = std::string("/parameters")) + .def("__len__", [](alps::params const & self) { return self.size(); }) + .def("__deepcopy__", ¶ms_deepcopy) + .def("__getitem__", ¶ms_getitem) + .def("__setitem__", ¶ms_setitem) + .def("__delitem__", ¶ms_delitem) + .def("__contains__", ¶ms_contains) + .def("__iter__", [](alps::params & self) { + // paramiterator yields pair; + // make_key_iterator projects out pair.first. + return nb::make_key_iterator( + nb::type(), + "key_iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("__str__", ¶ms_print) + .def("valueOrDefault", &value_or_default) + .def("save", &alps::params::save) + .def("load", ¶ms_load, + nb::arg("archive"), + nb::arg("path") = std::string("/parameters")); +} diff --git a/bindings/python/pyalps/cpp/ngs/random01.cpp b/bindings/python/pyalps/cpp/ngs/random01.cpp new file mode 100644 index 000000000..4efc47dee --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/random01.cpp @@ -0,0 +1,24 @@ +// Copyright (C) 2010 - 2013 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +namespace nb = nanobind; +NB_MODULE(pyngsrandom01_c, m) { + nb::class_(m, "random01") + .def(nb::init(), nb::arg("seed") = 42) + .def("__deepcopy__", + // copy.deepcopy() passes (self, memo); memo is unused. + [](alps::random01 const & self, nb::handle /*memo*/) { + return alps::random01(self); + }) + .def("__call__", + static_cast( + &alps::random01::operator())) + .def("save", &alps::random01::save) + .def("load", &alps::random01::load); +} diff --git a/bindings/python/pyalps/cpp/ngs/result.cpp b/bindings/python/pyalps/cpp/ngs/result.cpp new file mode 100644 index 000000000..5d7d6248e --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/result.cpp @@ -0,0 +1,181 @@ +// Copyright (C) 2010 - 2011 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace { +// Allocate a heap-owned 1-D numpy array of T and copy `n` elements from `src`. +template +nb::ndarray make_1d(T const * src, std::size_t n) { + T * buf = new T[n ? n : 1]; + if (n) std::memcpy(buf, src, n * sizeof(T)); + nb::capsule owner(buf, [](void * p) noexcept { + delete[] static_cast(p); + }); + std::size_t shape[1] = { n }; + return nb::ndarray(buf, 1, shape, owner); +} +} // namespace +namespace alps { + namespace detail { + template std::string short_print_python(T const & value) { + return cast(value); + } + template std::string short_print_python(std::vector const & value) { + switch (value.size()) { + case 0: + return "[]"; + case 1: + return "[" + short_print_python(value.front()) + "]"; + case 2: + return "[" + short_print_python(value.front()) + "," + short_print_python(value.back()) + "]"; + default: + return "[" + short_print_python(value.front()) + ",.." + short_print_python(value.size()) + "..," + short_print_python(value.back()) + "]"; + } + } + inline nb::object vec_to_numpy(std::vector const & v) { + return nb::cast(make_1d(v.data(), v.size())); + } + std::string mcresult_print(alps::mcresult const & self) { + if (self.count() == 0) + return "No Measurements"; + else if (self.is_type()) + return short_print_python(self.mean()) + "(" + short_print_python(self.count()) + ") " + + "+/-" + short_print_python(self.error()) + " " + + short_print_python(self.bins()) + "#" + short_print_python(self.bin_size()); + else if (self.is_type >()) + return short_print_python(self.mean >()) + "(" + short_print_python(self.count()) + ") " + + "+/-" + short_print_python(self.error >()) + " " + + short_print_python(self.bins >()) + "#" + short_print_python(self.bin_size()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + nb::object mcresult_mean(alps::mcresult const & self) { + if (self.is_type()) + return nb::float_(self.mean()); + else if (self.is_type >()) + return vec_to_numpy(self.mean >()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + nb::object mcresult_error(alps::mcresult const & self) { + if (self.is_type()) + return nb::float_(self.error()); + else if (self.is_type >()) + return vec_to_numpy(self.error >()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + nb::object mcresult_tau(alps::mcresult const & self) { + if (self.is_type()) + return nb::float_(self.tau()); + else if (self.is_type >()) + return vec_to_numpy(self.tau >()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + nb::object mcresult_variance(alps::mcresult const & self) { + if (self.is_type()) + return nb::float_(self.variance()); + else if (self.is_type >()) + return vec_to_numpy(self.variance >()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + nb::object mcresult_bins(alps::mcresult const & self) { + if (self.is_type()) + return vec_to_numpy(self.bins()); + else + throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); + } + alps::mcresult observable2result_export(alps::mcobservable const & obs) { + return alps::mcresult(obs); + } + } +} +NB_MODULE(pyngsresult_c, m) { + using namespace alps; + using R = alps::mcresult; + m.def("observable2result", &alps::detail::observable2result_export); + nb::class_(m, "result") + .def(nb::init<>()) + .def(nb::init()) + .def("__repr__", &alps::detail::mcresult_print) + .def("__deepcopy__", + [](R const & self, nb::handle /*memo*/) { + return R(self); + }) + .def("__abs__", static_cast(&abs)) + .def("__pow__", static_cast(&pow)) + .def_prop_ro("mean", &alps::detail::mcresult_mean) + .def_prop_ro("error", &alps::detail::mcresult_error) + .def_prop_ro("tau", &alps::detail::mcresult_tau) + .def_prop_ro("variance", &alps::detail::mcresult_variance) + .def_prop_ro("bins", &alps::detail::mcresult_bins) + .def_prop_ro("count", &R::count) + // mcresult's unary +/- operate on non-const self and return a + // reference (not a new value). Wrap them in lambdas that + // return a fresh copy, which is what Python's +obj/-obj expect. + .def("__pos__", [](R self) { return +self; }) + .def("__neg__", [](R self) { return -self; }) + // In-place operators — return self by reference so the original + // object is modified in place (Python's __i*__ semantics). + .def("__iadd__", [](R & self, R const & o) -> R & { return self += o; }, nb::is_operator()) + .def("__iadd__", [](R & self, double o) -> R & { return self += o; }, nb::is_operator()) + .def("__isub__", [](R & self, R const & o) -> R & { return self -= o; }, nb::is_operator()) + .def("__isub__", [](R & self, double o) -> R & { return self -= o; }, nb::is_operator()) + .def("__imul__", [](R & self, R const & o) -> R & { return self *= o; }, nb::is_operator()) + .def("__imul__", [](R & self, double o) -> R & { return self *= o; }, nb::is_operator()) + .def("__itruediv__", [](R & self, R const & o) -> R & { return self /= o; }, nb::is_operator()) + .def("__itruediv__", [](R & self, double o) -> R & { return self /= o; }, nb::is_operator()) + // Binary operators — left and right forms. nb::is_operator() + // marks them as Python operator overloads so mixed-type + // failures return NotImplemented rather than raising TypeError + // (giving Python's reflected operator machinery a chance). + .def("__add__", [](R const & a, R const & b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](R const & a, R const & b) { return b + a; }, nb::is_operator()) + .def("__add__", [](R const & a, double b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](R const & a, double b) { return b + a; }, nb::is_operator()) + .def("__sub__", [](R const & a, R const & b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](R const & a, R const & b) { return b - a; }, nb::is_operator()) + .def("__sub__", [](R const & a, double b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](R const & a, double b) { return b - a; }, nb::is_operator()) + .def("__mul__", [](R const & a, R const & b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](R const & a, R const & b) { return b * a; }, nb::is_operator()) + .def("__mul__", [](R const & a, double b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](R const & a, double b) { return b * a; }, nb::is_operator()) + .def("__truediv__", [](R const & a, R const & b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](R const & a, R const & b) { return b / a; }, nb::is_operator()) + .def("__truediv__", [](R const & a, double b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](R const & a, double b) { return b / a; }, nb::is_operator()) + .def("sq", static_cast(&sq)) + .def("cb", static_cast(&cb)) + .def("sqrt", static_cast(&sqrt)) + .def("cbrt", static_cast(&cbrt)) + .def("exp", static_cast(&exp)) + .def("log", static_cast(&log)) + .def("sin", static_cast(&sin)) + .def("cos", static_cast(&cos)) + .def("tan", static_cast(&tan)) + .def("sinh", static_cast(&sinh)) + .def("cosh", static_cast(&cosh)) + .def("tanh", static_cast(&tanh)) + .def("save", &R::save) + .def("load", &R::load); +} diff --git a/bindings/python/pyalps/cpp/ngs/results.cpp b/bindings/python/pyalps/cpp/ngs/results.cpp new file mode 100644 index 000000000..1b4b7d8ad --- /dev/null +++ b/bindings/python/pyalps/cpp/ngs/results.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2010 - 2011 by Lukas Gamper +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +// A generic map binder cannot be used for alps::mcresults because +// mcresults::erase(std::string const &) shadows the std::map::erase(iterator) +// that bind_map relies on for __delitem__. Synthesise the dict-like surface +// by hand instead. (Same applies to nanobind's bind_map.) +#define PY_ARRAY_UNIQUE_SYMBOL pyngsresults_PyArrayHandle +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace alps { + namespace detail { + std::string mcresults_print(alps::mcresults & self) { + std::stringstream sstr; + sstr << self; + return sstr.str(); + } + void mcresults_load(alps::mcresults & self, alps::hdf5::archive & ar, std::string const & path) { + std::string current = ar.get_context(); + ar.set_context(path); + self.load(ar); + ar.set_context(current); + } + } +} +NB_MODULE(pyngsresults_c, m) { + nb::class_(m, "results") + .def("__len__", [](alps::mcresults const & self) { return self.size(); }) + .def("__contains__", [](alps::mcresults const & self, std::string const & k) { + return self.has(k); + }) + .def("__getitem__", [](alps::mcresults & self, std::string const & k) -> alps::mcresult const & { + if (!self.has(k)) + throw nb::key_error(k.c_str()); + return self[k]; + }, + nb::rv_policy::reference_internal) + .def("__setitem__", [](alps::mcresults & self, std::string const & k, alps::mcresult const & v) { + self.insert(k, v); + }) + .def("__delitem__", [](alps::mcresults & self, std::string const & k) { + if (!self.has(k)) + throw nb::key_error(k.c_str()); + self.erase(k); + }) + .def("__iter__", [](alps::mcresults & self) { + return nb::make_key_iterator( + nb::type(), + "key_iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("keys", [](alps::mcresults & self) { + return nb::make_key_iterator( + nb::type(), + "key_iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("values", [](alps::mcresults & self) { + return nb::make_value_iterator( + nb::type(), + "value_iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("items", [](alps::mcresults & self) { + return nb::make_iterator( + nb::type(), + "item_iterator", + self.begin(), self.end()); + }, + nb::keep_alive<0, 1>()) + .def("__str__", &alps::detail::mcresults_print) + .def("save", &alps::mcresults::save) + .def("load", &alps::detail::mcresults_load); +} diff --git a/bindings/python/pyalps/cpp/numpy_compat.hpp b/bindings/python/pyalps/cpp/numpy_compat.hpp new file mode 100644 index 000000000..b23cafc0e --- /dev/null +++ b/bindings/python/pyalps/cpp/numpy_compat.hpp @@ -0,0 +1,95 @@ +// Copyright (C) 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +// +// Numpy interop without numpy headers. Construct numpy.ndarray +// instances from C++ buffers and consume incoming numpy arrays through +// nb::ndarray's DLPack/buffer view. The numpy package itself is loaded +// at runtime via nb::module_::import_("numpy"); pyalps already requires +// numpy as a runtime dependency. +#ifndef ALPS_PYTHON_NUMPY_COMPAT_HPP +#define ALPS_PYTHON_NUMPY_COMPAT_HPP +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace alps { + namespace python { + namespace nb_ = nanobind; + // numpy dtype strings, indexed by the corresponding C++ type. + // Used by make_numpy_array() / as_contiguous() to drive the + // numpy.empty(dtype=…) / numpy.ascontiguousarray(dtype=…) calls. + template struct numpy_dtype; + template <> struct numpy_dtype { static constexpr char const* name = "bool"; }; + template <> struct numpy_dtype { static constexpr char const* name = "int8"; }; + template <> struct numpy_dtype { static constexpr char const* name = "int16"; }; + template <> struct numpy_dtype { static constexpr char const* name = "int32"; }; + template <> struct numpy_dtype { static constexpr char const* name = "int64"; }; + template <> struct numpy_dtype { static constexpr char const* name = "uint8"; }; + template <> struct numpy_dtype { static constexpr char const* name = "uint16"; }; + template <> struct numpy_dtype { static constexpr char const* name = "uint32"; }; + template <> struct numpy_dtype { static constexpr char const* name = "uint64"; }; + template <> struct numpy_dtype { static constexpr char const* name = "float32"; }; + template <> struct numpy_dtype { static constexpr char const* name = "float64"; }; + template <> struct numpy_dtype> { static constexpr char const* name = "complex64"; }; + template <> struct numpy_dtype> { static constexpr char const* name = "complex128"; }; + // Allocates numpy.empty(shape, dtype=numpy_dtype::name) and + // memcpy's `data` (length = product(shape)) into it. Returns + // a writable numpy.ndarray. + template + inline nb_::object make_numpy_array(T const* data, + std::vector const& shape) { + nb_::object np = nb_::module_::import_("numpy"); + nb_::tuple shape_tuple = nb_::steal(PyTuple_New(static_cast(shape.size()))); + for (std::size_t i = 0; i < shape.size(); ++i) + PyTuple_SET_ITEM(shape_tuple.ptr(), static_cast(i), + PyLong_FromUnsignedLongLong(shape[i])); + nb_::object arr = np.attr("empty")( + shape_tuple, nb_::arg("dtype") = numpy_dtype::name); + // Bridge the freshly-allocated numpy buffer through nb::ndarray + // to get a writable raw pointer. + auto nd = nb_::cast>(arr); + std::size_t total = 1; + for (auto s : shape) total *= s; + if (total > 0) + std::memcpy(nd.data(), data, total * sizeof(T)); + return arr; + } + template + inline nb_::object make_numpy_array(std::vector const& v) { + return make_numpy_array(v.data(), {v.size()}); + } + // Strong-ref'd C-contiguous view onto a numpy array of dtype T. + // The owner handle keeps the array alive for the lifetime of + // the view; data() / shape() / ndim() forward to the ndarray. + template + struct contiguous_view { + nb_::object owner; + nb_::ndarray nd; + T const* data() const { return nd.data(); } + std::size_t ndim() const { return nd.ndim(); } + std::size_t shape(int i) const { return nd.shape(i); } + }; + // Coerces `obj` to a C-contiguous numpy.ndarray of dtype T via + // numpy.ascontiguousarray. Always produces a contiguous + + // correctly-typed buffer (numpy copies if the input doesn't + // already match). Equivalent in spirit to nanobind's + // py::array_t + // parameter form, just routed through numpy at runtime instead + // of through the numpy C headers at compile time. + template + inline contiguous_view as_contiguous(nb_::handle obj) { + nb_::object np = nb_::module_::import_("numpy"); + nb_::object arr = np.attr("ascontiguousarray")( + obj, nb_::arg("dtype") = numpy_dtype::name); + auto nd = nb_::cast>(arr); + return contiguous_view{std::move(arr), std::move(nd)}; + } + } // namespace python +} // namespace alps +#endif // ALPS_PYTHON_NUMPY_COMPAT_HPP diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp new file mode 100644 index 000000000..b5ecd5607 --- /dev/null +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -0,0 +1,368 @@ +// Copyright (C) 1994-2010 by Ping Nang Ma , +// Lukas Gamper , +// Matthias Troyer , +// Maximilian Poprawe +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "numpy_compat.hpp" +#include "save_observable_to_hdf5.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace alps { + namespace alea { + // Wraps a scalar-valarray observable (RealVectorObservable / + // RealVectorTimeSeriesObservable) so Python sees numpy arrays + // in / out instead of std::valarray. + template + class WrappedValarrayObservable { + using element_type = typename T::value_type::value_type; + public: + WrappedValarrayObservable(std::string const & name, int s = 0) + : obs(name, s) + {} + // Copy the ndarray into a valarray and feed it to the observable. + void push(nb::handle arr) { + auto view = alps::python::as_contiguous(arr); + if (view.ndim() != 1) + throw std::invalid_argument( + "RealVectorObservable.push: expected 1-D array"); + std::size_t n = static_cast(view.shape(0)); + std::valarray v(n); + double const * data = view.data(); + for (std::size_t i = 0; i < n; ++i) + v[i] = static_cast(data[i]); + obs << v; + } + std::string representation() const { return obs.representation(); } + // Turn an alps::numeric std::valarray-like view into a + // 1-D numpy.ndarray (dtype=float64) by copying through + // numpy.empty + buffer protocol. + template + static nb::object _to_numpy(U const & v) { + std::size_t n = static_cast(v.size()); + std::vector tmp(n); + for (std::size_t i = 0; i < n; ++i) + tmp[i] = static_cast(v[i]); + return alps::python::make_numpy_array(tmp.data(), {n}); + } + nb::object mean() const { return _to_numpy(obs.mean()); } + nb::object error() const { return _to_numpy(obs.error()); } + nb::object tau() const { return _to_numpy(obs.tau()); } + nb::object variance() const { return _to_numpy(obs.variance()); } + void save(std::string const & filename) const { + alps::hdf5::archive ar(filename, "a"); + ar["/simulation/results/" + obs.representation()] << obs; + } + typename T::count_type count() const { return obs.count(); } + typename T::convergence_type converged_errors() const { return obs.converged_errors(); } + private: + T obs; + }; + } // namespace alea +} // namespace alps +namespace { +// Build a 1-D numpy.ndarray (dtype=float64) from any sequence-like +// alps container (std::vector, std::valarray). +template +nb::object seq_to_numpy(Container const & v) { + std::size_t n = static_cast(v.size()); + std::vector tmp(n); + for (std::size_t i = 0; i < n; ++i) + tmp[i] = static_cast(v[i]); + return alps::python::make_numpy_array(tmp.data(), {n}); +} +// Copy a numpy array into a std::vector. Used when the +// caller still instantiates mctimeseries from a +// Python array. +template +std::vector +numpy_to_vector(nb::handle arr) { + auto view = alps::python::as_contiguous(arr); + if (view.ndim() != 1) + throw std::invalid_argument( + "mctimeseries ctor: expected 1-D array"); + std::size_t n = static_cast(view.shape(0)); + std::vector out(n); + double const * data = view.data(); + for (std::size_t i = 0; i < n; ++i) + out[i] = static_cast(data[i]); + return out; +} +// __repr__ helper for any ALPS type with an ostream operator. +template +std::string stream_repr(T const & x) { + std::ostringstream ss; + ss << x; + return ss.str(); +} +template +std::string value_with_error_repr(alps::alea::value_with_error const & v) { + std::ostringstream ss; + ss << v.mean() << " +/- " << v.error(); + return ss.str(); +} +// Numpy-returning wrappers for vector-valued alps::alea free +// functions (mean, variance, uncorrelated_error, binning_error) — +// the scalar overloads are bound directly and nanobind casts +// their `double` return to a Python float automatically. +template +nb::object mean_vector(T const & x) { + return seq_to_numpy(alps::alea::mean(x)); +} +template +nb::object variance_vector(T const & x) { + return seq_to_numpy(alps::alea::variance(x)); +} +// mctimeseries.timeseries() returns std::vector; hand +// back to Python as numpy. For scalar ValueType we pack 1-D; for +// vector ValueType we pack 2-D. mctimeseries_view has the +// same surface. +template +nb::object ts_to_numpy_scalar(TS const & ts) { + auto const & v = ts.timeseries(); + return seq_to_numpy(v); +} +template +nb::object ts_to_numpy_vector_rows(TS const & ts) { + auto const & rows = ts.timeseries(); + if (rows.empty()) + return alps::python::make_numpy_array( + static_cast(nullptr), {std::size_t{0}, std::size_t{0}}); + std::size_t nrows = rows.size(); + std::size_t ncols = rows.front().size(); + for (auto const & row : rows) + if (row.size() != ncols) + throw std::runtime_error("mctimeseries has ragged rows; cannot shape as numpy 2-D"); + std::vector flat(nrows * ncols); + double * dst = flat.data(); + for (auto const & row : rows) { + for (std::size_t j = 0; j < ncols; ++j) + *dst++ = static_cast(row[j]); + } + return alps::python::make_numpy_array(flat.data(), {nrows, ncols}); +} +} // namespace +NB_MODULE(pyalea_c, m) { + m.doc() = "ALPS alea bindings (nanobind)"; + // ─── scalar-valarray observables ───────────────────────────────── + using RealVecObs = alps::alea::WrappedValarrayObservable; + using RealVecTsObs = alps::alea::WrappedValarrayObservable; + #define ALPS_PY_EXPORT_VECTOROBSERVABLE(Wrapper, PyName) \ + nb::class_(m, PyName) \ + .def("__init__", \ + [](Wrapper * self, std::string name, int bins) { \ + new (self) Wrapper(name, bins); \ + }, \ + nb::arg("name"), nb::arg("bins") = 0) \ + .def("__repr__", &Wrapper::representation) \ + .def("__deepcopy__", \ + [](Wrapper const & self, nb::handle /*memo*/) { \ + return Wrapper(self); \ + }) \ + .def("__lshift__", &Wrapper::push, nb::arg("array")) \ + .def("save", &Wrapper::save, nb::arg("filename")) \ + .def_prop_ro("mean", &Wrapper::mean) \ + .def_prop_ro("error", &Wrapper::error) \ + .def_prop_ro("tau", &Wrapper::tau) \ + .def_prop_ro("variance", &Wrapper::variance) \ + .def_prop_ro("count", &Wrapper::count) \ + .def_prop_ro("converged_errors", &Wrapper::converged_errors) + ALPS_PY_EXPORT_VECTOROBSERVABLE(RealVecObs, "RealVectorObservable"); + ALPS_PY_EXPORT_VECTOROBSERVABLE(RealVecTsObs, "RealVectorTimeSeriesObservable"); + #undef ALPS_PY_EXPORT_VECTOROBSERVABLE + // ─── scalar simple observables ─────────────────────────────────── + #define ALPS_PY_EXPORT_SIMPLEOBSERVABLE(AlpsClass, PyName) \ + nb::class_(m, PyName) \ + .def("__init__", \ + [](alps::AlpsClass * self, std::string name, int bins) { \ + new (self) alps::AlpsClass(name, bins); \ + }, \ + nb::arg("name"), nb::arg("bins") = 0) \ + .def("__deepcopy__", \ + [](alps::AlpsClass const & self, nb::handle /*memo*/) { \ + return alps::AlpsClass(self); \ + }) \ + .def("__repr__", &alps::AlpsClass::representation) \ + .def("__lshift__", &alps::AlpsClass::operator<<) \ + .def("save", &alps::python::save_observable_to_hdf5, \ + nb::arg("filename")) \ + .def_prop_ro("mean", &alps::AlpsClass::mean) \ + .def_prop_ro("error", \ + static_cast( \ + &alps::AlpsClass::error)) \ + .def_prop_ro("tau", &alps::AlpsClass::tau) \ + .def_prop_ro("variance", &alps::AlpsClass::variance) \ + .def_prop_ro("count", &alps::AlpsClass::count) \ + .def_prop_ro("converged_errors", &alps::AlpsClass::converged_errors) + ALPS_PY_EXPORT_SIMPLEOBSERVABLE(RealObservable, "RealObservable"); + ALPS_PY_EXPORT_SIMPLEOBSERVABLE(RealTimeSeriesObservable, "RealTimeSeriesObservable"); + #undef ALPS_PY_EXPORT_SIMPLEOBSERVABLE + // ─── value_with_error ──────────────────────────────────────────── + nb::class_>(m, "ValueWithError") + .def(nb::init(), + nb::arg("mean") = 0.0, nb::arg("error") = 0.0) + .def_prop_ro("mean", &alps::alea::value_with_error::mean) + .def_prop_ro("error", &alps::alea::value_with_error::error) + .def("__repr__", &value_with_error_repr); + // ─── StdPairDouble ───────────────────────────────────────────── + // nanobind's STL caster already registered std::pair as a + // Python tuple converter via the stl.h header; nanobind takes the + // same path via . Binding it again as a + // class_ would fight that, so use a thin attribute-access + // wrapper instead, and give integrated_autocorrelation_time a + // Python-side signature that accepts either StdPairDouble or a + // plain (float, float) tuple. + struct StdPairDouble { + double first{0.0}; + double second{0.0}; + StdPairDouble() = default; + StdPairDouble(double f, double s) : first(f), second(s) {} + operator std::pair() const { return {first, second}; } + }; + nb::class_(m, "StdPairDouble", + "Pair of (fit slope, fit intercept) returned by the autocorrelation fit helpers.") + .def(nb::init<>()) + .def(nb::init(), nb::arg("first"), nb::arg("second")) + .def_rw("first", &StdPairDouble::first) + .def_rw("second", &StdPairDouble::second) + .def("__repr__", [](StdPairDouble const & p) { + std::ostringstream ss; + ss << "StdPairDouble(" << p.first << ", " << p.second << ")"; + return ss.str(); + }); + // ─── mctimeseries / mctimeseries_view bindings ──────────── + // + // Numpy-ctor: take any handle and copy through numpy_to_vector. + // The timeseries() method returns numpy arrays directly; the + // 2-D overload for vector goes through the + // ts_to_numpy_vector_rows helper. + #define ALPS_PY_EXPORT_MCTIMESERIES_SCALAR(Value, PyName) \ + nb::class_>(m, PyName) \ + .def(nb::init<>()) \ + .def("__init__", \ + [](alps::alea::mctimeseries * self, nb::handle a) { \ + new (self) alps::alea::mctimeseries( \ + numpy_to_vector(a)); \ + }) \ + .def(nb::init>()) \ + .def("timeseries", [](alps::alea::mctimeseries const & self) { \ + return ts_to_numpy_scalar(self); \ + }) \ + .def_prop_ro("size", &alps::alea::mctimeseries::size) \ + .def("__repr__", &stream_repr>); \ + nb::class_>(m, PyName "View") \ + .def(nb::init>()) \ + .def(nb::init>()) \ + .def("timeseries", [](alps::alea::mctimeseries_view const & self) { \ + return ts_to_numpy_scalar(self); \ + }) \ + .def_prop_ro("size", &alps::alea::mctimeseries_view::size) \ + .def("__repr__", &stream_repr>) + ALPS_PY_EXPORT_MCTIMESERIES_SCALAR(double, "MCScalarTimeseries"); + #undef ALPS_PY_EXPORT_MCTIMESERIES_SCALAR + // Vector-valued mctimeseries: ctor from a 2-D numpy array, rows + // are time samples. timeseries() returns 2-D. + using VecTs = alps::alea::mctimeseries>; + using VecTsV = alps::alea::mctimeseries_view>; + using VecMcD = alps::alea::mcdata>; + nb::class_(m, "MCVectorTimeseries") + .def(nb::init<>()) + .def("__init__", [](VecTs * self, nb::handle a) { + auto view = alps::python::as_contiguous(a); + if (view.ndim() != 2) + throw std::invalid_argument( + "MCVectorTimeseries ctor: expected 2-D array"); + std::size_t nrows = static_cast(view.shape(0)); + std::size_t ncols = static_cast(view.shape(1)); + std::vector> rows(nrows); + double const * data = view.data(); + for (std::size_t i = 0; i < nrows; ++i) { + rows[i].resize(ncols); + for (std::size_t j = 0; j < ncols; ++j) + rows[i][j] = data[i * ncols + j]; + } + new (self) VecTs(rows); + }) + .def(nb::init()) + .def("timeseries", [](VecTs const & self) { return ts_to_numpy_vector_rows(self); }) + .def_prop_ro("size", &VecTs::size) + .def("__repr__", &stream_repr); + nb::class_(m, "MCVectorTimeseriesView") + .def(nb::init()) + .def(nb::init()) + .def("timeseries", [](VecTsV const & self) { return ts_to_numpy_vector_rows(self); }) + .def_prop_ro("size", &VecTsV::size) + .def("__repr__", &stream_repr); + // ─── alps::alea free functions over mcdata / mctimeseries ──────── + #define DEF_ALL(name, fn) \ + /* scalar-valued */ \ + m.def(name, static_cast const &)>(&fn)); \ + m.def(name, static_cast const &)>(&fn)); \ + m.def(name, static_cast const &)>(&fn)); + // size — works for both scalar and vector value types. + m.def("size", static_cast const &)>(&alps::size)); + m.def("size", static_cast const &)>(&alps::size)); + m.def("size", static_cast const &)>(&alps::size)); + m.def("size", static_cast> const &)>(&alps::size)); + m.def("size", static_cast> const &)>(&alps::size)); + m.def("size", static_cast> const &)>(&alps::size)); + // mean — scalar overloads return double, vector overloads return numpy. + DEF_ALL("mean", alps::alea::mean) + m.def("mean", &mean_vector>>); + m.def("mean", &mean_vector>>); + m.def("mean", &mean_vector>>); + // variance — same pattern. + DEF_ALL("variance", alps::alea::variance) + m.def("variance", &variance_vector>>); + m.def("variance", &variance_vector>>); + m.def("variance", &variance_vector>>); + // integrated_autocorrelation_time — scalar only. The C++ signature + // takes the (slope, intercept) pair by const-ref. + m.def("integrated_autocorrelation_time", + static_cast const &, + std::pair const &)>( + &alps::alea::integrated_autocorrelation_time)); + m.def("integrated_autocorrelation_time", + static_cast const &, + std::pair const &)>( + &alps::alea::integrated_autocorrelation_time)); + // running_mean / reverse_running_mean — scalar only, mctimeseries-valued. + m.def("running_mean", + static_cast (*)(alps::alea::mcdata const &)>( + &alps::alea::running_mean)); + m.def("running_mean", + static_cast (*)(alps::alea::mctimeseries const &)>( + &alps::alea::running_mean)); + m.def("running_mean", + static_cast (*)(alps::alea::mctimeseries_view const &)>( + &alps::alea::running_mean)); + m.def("reverse_running_mean", + static_cast (*)(alps::alea::mcdata const &)>( + &alps::alea::reverse_running_mean)); + m.def("reverse_running_mean", + static_cast (*)(alps::alea::mctimeseries const &)>( + &alps::alea::reverse_running_mean)); + m.def("reverse_running_mean", + static_cast (*)(alps::alea::mctimeseries_view const &)>( + &alps::alea::reverse_running_mean)); + #undef DEF_ALL +} diff --git a/bindings/python/pyalps/cpp/pymcdata.cpp b/bindings/python/pyalps/cpp/pymcdata.cpp new file mode 100644 index 000000000..017299470 --- /dev/null +++ b/bindings/python/pyalps/cpp/pymcdata.cpp @@ -0,0 +1,329 @@ +// Copyright (C) 1994-2010 by Ping Nang Ma , +// Lukas Gamper , +// Matthias Troyer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include "numpy_compat.hpp" +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +namespace alps { + namespace python { + // Build a 1-D numpy.ndarray (dtype=float64) from a + // std::vector. Allocates a fresh array via + // numpy.empty + memcpy through the buffer protocol — no + // numpy headers. + inline nb::object vec_to_numpy(std::vector const & v) { + return make_numpy_array(v.data(), {v.size()}); + } + // Build a 2-D numpy.ndarray from a vector>. + // Rows must be equal-length; on mismatch throw a value error + // (mcdata doesn't produce ragged bins/jackknife tables). + inline nb::object matrix_to_numpy(std::vector> const & m) { + std::size_t rows = m.size(); + std::size_t cols = rows > 0 ? m.front().size() : 0; + for (auto const & row : m) { + if (row.size() != cols) + throw std::runtime_error("mcdata returned ragged 2-D table; refusing to convert to numpy"); + } + std::vector flat(rows * cols); + double * dst = flat.data(); + for (auto const & row : m) { + std::copy(row.begin(), row.end(), dst); + dst += cols; + } + return make_numpy_array(flat.data(), {rows, cols}); + } + // __repr__ for mcdata — " +/- ". + template + std::string print_mcdata_scalar(alps::alea::mcdata const & self) { + std::ostringstream ss; + ss << self.mean() << " +/- " << self.error(); + return ss.str(); + } + // __repr__ for mcdata> — newline-joined scalar reprs. + // The const_iterator's operator-> returns through boost::addressof + // on a rvalue scalar mcdata, which triggers the deleted + // overload; take a local copy instead. + template + std::string print_mcdata_vector(alps::alea::mcdata> const & self) { + std::ostringstream ss; + bool first = true; + for (auto it = self.begin(); it != self.end(); ++it) { + if (!first) ss << "\n"; + first = false; + alps::alea::mcdata entry = *it; + ss << entry.mean() << " +/- " << entry.error(); + } + return ss.str(); + } + // __format__ for mcdata — defers to builtins.format on + // mean and error separately, then joins with " +/- ". + template + std::string format_mcdata_scalar(alps::alea::mcdata const & self, + std::string const & spec) { + nb::object fmt = nb::module_::import_("builtins").attr("format"); + std::string m = nb::cast(fmt(self.mean(), spec)); + std::string e = nb::cast(fmt(self.error(), spec)); + return m + " +/- " + e; + } + template + std::string format_mcdata_vector(alps::alea::mcdata> const & self, + std::string const & spec) { + std::ostringstream ss; + bool first = true; + for (auto it = self.begin(); it != self.end(); ++it) { + if (!first) ss << "\n"; + first = false; + alps::alea::mcdata entry = *it; + ss << format_mcdata_scalar(entry, spec); + } + return ss.str(); + } + // Indexing: mcdata>[i] returns a scalar mcdata + // view; mcdata>[slice] returns a sliced + // mcdata>. + template + nb::object mcdata_vector_getitem(alps::alea::mcdata> & data, + nb::object const & key) { + std::size_t n = data.mean().size(); + if (nb::isinstance(key)) { + nb::slice s = nb::borrow(key); + auto [start, stop, step, slicelength] = s.compute(n); + if (step != 1) + throw nb::index_error("slice step size not supported."); + if (start > stop) + return nb::cast(alps::alea::mcdata>()); + return nb::cast(alps::alea::mcdata>( + data, start, stop)); + } + long index = nb::cast(key); + if (index < 0) + index += static_cast(n); + if (index < 0 || static_cast(index) >= n) + throw nb::index_error("Index out of range"); + return nb::cast(alps::alea::mcdata( + data, static_cast(index))); + } + template + bool mcdata_vector_contains(alps::alea::mcdata> & data, + nb::object const & key) { + // Best-effort: accept either a scalar mcdata or a value + // that casts cleanly; anything else is "not in". + // mcdata>::const_iterator does not fully + // model std::ranges::input_range (the iterator traits + // needed for borrowed_iterator_t aren't wired up), so the + // ranges-form of std::find is ill-formed here. Keep the + // classical iterator pair. + try { + auto probe = nb::cast>(key); + return std::find(data.begin(), data.end(), probe) != data.end(); + } catch (nb::cast_error const &) { + } + return false; + } + } +} +NB_MODULE(pymcdata_c, m) { + using alps::alea::mcdata; + namespace pymod = alps::python; + using Scalar = mcdata; + using Vector = mcdata>; + // mcdata's transcendentals (sq, cb, sqrt, cbrt, exp, log, sin, …) + // live in namespace alps::alea and are found via ADL when we pass + // an mcdata argument. Capture each as a lambda so the binding + // doesn't have to cast through overloaded name lookup, which + // fights 's own abs/pow/sqrt etc. sitting in global scope. + nb::class_(m, "MCScalarData", + "Scalar Monte Carlo data. Supports +, -, *, /, +=, -=, *=, /=, " + "abs, pow, sq, cb, sqrt, cbrt, exp, log, sin, cos, tan, sinh, cosh, tanh.") + .def(nb::init<>()) + .def(nb::init(), nb::arg("mean")) + .def(nb::init(), nb::arg("mean"), nb::arg("error")) + .def_prop_ro("mean", [](Scalar const & v) { return v.mean(); }) + .def_prop_ro("error", [](Scalar const & v) { return v.error(); }) + .def_prop_ro("tau", [](Scalar const & v) { return v.tau(); }) + .def_prop_ro("variance", [](Scalar const & v) { return v.variance(); }) + .def_prop_ro("bins", [](Scalar const & v) { + return pymod::vec_to_numpy(v.bins()); + }) + .def_prop_ro("jackknife",[](Scalar const & v) { + return pymod::vec_to_numpy(v.jackknife()); + }) + .def_prop_ro("count", &Scalar::count) + .def("__repr__", &pymod::print_mcdata_scalar) + .def("__format__", &pymod::format_mcdata_scalar, + nb::arg("format_spec")) + .def("__deepcopy__", + [](Scalar const & self, nb::handle /*memo*/) { + return Scalar(self); + }) + .def("__abs__", [](Scalar x) { using alps::alea::abs; return abs(std::move(x)); }) + .def("__pow__", [](Scalar x, double e) { using alps::alea::pow; return pow(std::move(x), e); }) + // Unary - / + on mcdata produce new values; wrap manually + // because the library's operator+()/-() signatures aren't + // const-returning (which is what nb::self expects). + .def("__pos__", [](Scalar self) { return +self; }) + .def("__neg__", [](Scalar self) { return -self; }) + // In-place operators — modify self in place, return reference. + .def("__iadd__", [](Scalar & s, Scalar const & o) -> Scalar & { return s += o; }, nb::is_operator()) + .def("__iadd__", [](Scalar & s, double o) -> Scalar & { return s += o; }, nb::is_operator()) + .def("__isub__", [](Scalar & s, Scalar const & o) -> Scalar & { return s -= o; }, nb::is_operator()) + .def("__isub__", [](Scalar & s, double o) -> Scalar & { return s -= o; }, nb::is_operator()) + .def("__imul__", [](Scalar & s, Scalar const & o) -> Scalar & { return s *= o; }, nb::is_operator()) + .def("__imul__", [](Scalar & s, double o) -> Scalar & { return s *= o; }, nb::is_operator()) + .def("__itruediv__", [](Scalar & s, Scalar const & o) -> Scalar & { return s /= o; }, nb::is_operator()) + .def("__itruediv__", [](Scalar & s, double o) -> Scalar & { return s /= o; }, nb::is_operator()) + // Binary +/-/*//: forward and reflected forms. nb::is_operator() + // marks them so mixed-type failures return NotImplemented and + // Python's reflected operator machinery gets a turn. + .def("__add__", [](Scalar const & a, Scalar const & b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](Scalar const & a, Scalar const & b) { return b + a; }, nb::is_operator()) + .def("__add__", [](Scalar const & a, double b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](Scalar const & a, double b) { return b + a; }, nb::is_operator()) + .def("__sub__", [](Scalar const & a, Scalar const & b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](Scalar const & a, Scalar const & b) { return b - a; }, nb::is_operator()) + .def("__sub__", [](Scalar const & a, double b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](Scalar const & a, double b) { return b - a; }, nb::is_operator()) + .def("__mul__", [](Scalar const & a, Scalar const & b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Scalar const & a, Scalar const & b) { return b * a; }, nb::is_operator()) + .def("__mul__", [](Scalar const & a, Vector const & b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Scalar const & a, Vector const & b) { return b * a; }, nb::is_operator()) + .def("__mul__", [](Scalar const & a, double b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Scalar const & a, double b) { return b * a; }, nb::is_operator()) + .def("__truediv__", [](Scalar const & a, Scalar const & b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](Scalar const & a, Scalar const & b) { return b / a; }, nb::is_operator()) + .def("__truediv__", [](Scalar const & a, double b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](Scalar const & a, double b) { return b / a; }, nb::is_operator()) + .def("sq", [](Scalar x) { using alps::alea::sq; return sq(std::move(x)); }) + .def("cb", [](Scalar x) { using alps::alea::cb; return cb(std::move(x)); }) + .def("sqrt", [](Scalar x) { using alps::alea::sqrt; return sqrt(std::move(x)); }) + .def("cbrt", [](Scalar x) { using alps::alea::cbrt; return cbrt(std::move(x)); }) + .def("exp", [](Scalar x) { using alps::alea::exp; return exp(std::move(x)); }) + .def("log", [](Scalar x) { using alps::alea::log; return log(std::move(x)); }) + .def("sin", [](Scalar x) { using alps::alea::sin; return sin(std::move(x)); }) + .def("cos", [](Scalar x) { using alps::alea::cos; return cos(std::move(x)); }) + .def("tan", [](Scalar x) { using alps::alea::tan; return tan(std::move(x)); }) + .def("sinh", [](Scalar x) { using alps::alea::sinh; return sinh(std::move(x)); }) + .def("cosh", [](Scalar x) { using alps::alea::cosh; return cosh(std::move(x)); }) + .def("tanh", [](Scalar x) { using alps::alea::tanh; return tanh(std::move(x)); }) + .def("set_bin_size", &Scalar::set_bin_size) + .def("set_bin_number", &Scalar::set_bin_number) + .def("discard_bins", &Scalar::discard_bins) + .def("merge", static_cast(&Scalar::merge)) + .def("save", static_cast(&Scalar::save), + nb::arg("filename"), nb::arg("observable_name")) + .def("load", static_cast(&Scalar::load), + nb::arg("filename"), nb::arg("observable_name")); + nb::class_(m, "MCVectorData", + "Vector-valued Monte Carlo data.") + .def(nb::init<>()) + .def(nb::init>(), nb::arg("mean")) + .def(nb::init, std::vector>(), + nb::arg("mean"), nb::arg("error")) + .def("__len__", + [](Vector & v) { + return v.mean().size(); + }) + .def("__getitem__", &pymod::mcdata_vector_getitem) + .def("__contains__", &pymod::mcdata_vector_contains) + .def_prop_ro("mean", [](Vector const & v) { + return pymod::vec_to_numpy(v.mean()); + }) + .def_prop_ro("error", [](Vector const & v) { + return pymod::vec_to_numpy(v.error()); + }) + .def_prop_ro("tau", [](Vector const & v) { + return pymod::vec_to_numpy(v.tau()); + }) + .def_prop_ro("variance", [](Vector const & v) { + return pymod::vec_to_numpy(v.variance()); + }) + .def_prop_ro("bins", [](Vector const & v) { + return pymod::matrix_to_numpy(v.bins()); + }) + .def_prop_ro("jackknife",[](Vector const & v) { + return pymod::matrix_to_numpy(v.jackknife()); + }) + .def_prop_ro("count", &Vector::count) + .def("__repr__", &pymod::print_mcdata_vector) + .def("__format__", &pymod::format_mcdata_vector, + nb::arg("format_spec")) + .def("__deepcopy__", + [](Vector const & self, nb::handle /*memo*/) { + return Vector(self); + }) + .def("__abs__", [](Vector x) { using alps::alea::abs; return abs(std::move(x)); }) + .def("__pow__", [](Vector x, double e) { using alps::alea::pow; return pow(std::move(x), e); }) + .def("__pos__", [](Vector self) { return +self; }) + .def("__neg__", [](Vector self) { return -self; }) + .def("__eq__", [](Vector const & a, Vector const & b) { return a == b; }, nb::is_operator()) + // In-place operators. + .def("__iadd__", [](Vector & s, Vector const & o) -> Vector & { return s += o; }, nb::is_operator()) + .def("__iadd__", [](Vector & s, std::vector const & o) -> Vector & { return s += o; }, nb::is_operator()) + .def("__isub__", [](Vector & s, Vector const & o) -> Vector & { return s -= o; }, nb::is_operator()) + .def("__isub__", [](Vector & s, std::vector const & o) -> Vector & { return s -= o; }, nb::is_operator()) + .def("__imul__", [](Vector & s, Vector const & o) -> Vector & { return s *= o; }, nb::is_operator()) + .def("__imul__", [](Vector & s, std::vector const & o) -> Vector & { return s *= o; }, nb::is_operator()) + .def("__itruediv__", [](Vector & s, Vector const & o) -> Vector & { return s /= o; }, nb::is_operator()) + .def("__itruediv__", [](Vector & s, std::vector const & o) -> Vector & { return s /= o; }, nb::is_operator()) + // Binary operators — Vector ↔ Vector / Scalar / vector / double. + .def("__add__", [](Vector const & a, Vector const & b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](Vector const & a, Vector const & b) { return b + a; }, nb::is_operator()) + .def("__add__", [](Vector const & a, std::vector const & b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](Vector const & a, std::vector const & b) { return b + a; }, nb::is_operator()) + .def("__sub__", [](Vector const & a, Vector const & b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](Vector const & a, Vector const & b) { return b - a; }, nb::is_operator()) + .def("__sub__", [](Vector const & a, std::vector const & b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](Vector const & a, std::vector const & b) { return b - a; }, nb::is_operator()) + .def("__mul__", [](Vector const & a, Vector const & b) { return a * b; }, nb::is_operator()) + .def("__mul__", [](Vector const & a, Scalar const & b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Vector const & a, Scalar const & b) { return b * a; }, nb::is_operator()) + .def("__rmul__", [](Vector const & a, Vector const & b) { return b * a; }, nb::is_operator()) + .def("__mul__", [](Vector const & a, std::vector const & b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Vector const & a, std::vector const & b) { return b * a; }, nb::is_operator()) + .def("__truediv__", [](Vector const & a, Vector const & b) { return a / b; }, nb::is_operator()) + .def("__truediv__", [](Vector const & a, Scalar const & b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](Vector const & a, Vector const & b) { return b / a; }, nb::is_operator()) + .def("__truediv__", [](Vector const & a, std::vector const & b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](Vector const & a, std::vector const & b) { return b / a; }, nb::is_operator()) + // Vector ↔ double. + .def("__add__", [](Vector const & a, double b) { return a + b; }, nb::is_operator()) + .def("__radd__", [](Vector const & a, double b) { return b + a; }, nb::is_operator()) + .def("__sub__", [](Vector const & a, double b) { return a - b; }, nb::is_operator()) + .def("__rsub__", [](Vector const & a, double b) { return b - a; }, nb::is_operator()) + .def("__mul__", [](Vector const & a, double b) { return a * b; }, nb::is_operator()) + .def("__rmul__", [](Vector const & a, double b) { return b * a; }, nb::is_operator()) + .def("__truediv__", [](Vector const & a, double b) { return a / b; }, nb::is_operator()) + .def("__rtruediv__", [](Vector const & a, double b) { return b / a; }, nb::is_operator()) + .def("sq", [](Vector x) { using alps::alea::sq; return sq(std::move(x)); }) + .def("cb", [](Vector x) { using alps::alea::cb; return cb(std::move(x)); }) + .def("sqrt", [](Vector x) { using alps::alea::sqrt; return sqrt(std::move(x)); }) + .def("cbrt", [](Vector x) { using alps::alea::cbrt; return cbrt(std::move(x)); }) + .def("exp", [](Vector x) { using alps::alea::exp; return exp(std::move(x)); }) + .def("log", [](Vector x) { using alps::alea::log; return log(std::move(x)); }) + .def("sin", [](Vector x) { using alps::alea::sin; return sin(std::move(x)); }) + .def("cos", [](Vector x) { using alps::alea::cos; return cos(std::move(x)); }) + .def("tan", [](Vector x) { using alps::alea::tan; return tan(std::move(x)); }) + .def("sinh", [](Vector x) { using alps::alea::sinh; return sinh(std::move(x)); }) + .def("cosh", [](Vector x) { using alps::alea::cosh; return cosh(std::move(x)); }) + .def("tanh", [](Vector x) { using alps::alea::tanh; return tanh(std::move(x)); }) + .def("set_bin_size", &Vector::set_bin_size) + .def("set_bin_number", &Vector::set_bin_number) + .def("discard_bins", &Vector::discard_bins) + .def("merge", static_cast(&Vector::merge)) + .def("save", static_cast(&Vector::save), + nb::arg("filename"), nb::arg("observable_name")) + .def("load", static_cast(&Vector::load), + nb::arg("filename"), nb::arg("observable_name")); +} diff --git a/bindings/python/pyalps/cpp/pytools.cpp b/bindings/python/pyalps/cpp/pytools.cpp new file mode 100644 index 000000000..3edc408ff --- /dev/null +++ b/bindings/python/pyalps/cpp/pytools.cpp @@ -0,0 +1,52 @@ +// Copyright (C) 1994-2009 by Ping Nang Ma , +// Matthias Troyer , +// Bela Bauer +// 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace nb = nanobind; +typedef boost::variate_generator > random_01; +class WrappedRNG : public random_01 +{ +public: + WrappedRNG(int seed = 0) + : random_01(boost::mt19937(seed), boost::uniform_01()) + { + } +}; +NB_MODULE(pytools_c, m) { + m.doc() = "ALPS tools bindings (nanobind)"; + m.def("convert2xml", + &alps::convert2xml, + "Convert an ALPS file to XML. Returns the path to the XML file."); + m.def("hdf5_name_encode", + &alps::hdf5_name_encode, + "Escape a string for use inside an HDF5 path name."); + m.def("hdf5_name_decode", + &alps::hdf5_name_decode, + "Un-escape a string taken from an HDF5 path name."); + m.def("search_xml_library_path", + &alps::search_xml_library_path, + "Resolve an ALPS library XML / XSL file to its full path."); + nb::class_(m, "rng", + "Mersenne-Twister uniform random number generator in [0, 1).") + .def(nb::init(), nb::arg("seed") = 0) + .def("__deepcopy__", + [](WrappedRNG const & self, nb::handle /*memo*/) { + return WrappedRNG(self); + }, + "Return a fresh copy of the RNG carrying the same state.") + .def("__call__", + static_cast( + &WrappedRNG::operator()), + "Return a uniform random number in [0, 1)."); +} diff --git a/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp b/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp new file mode 100644 index 000000000..556634686 --- /dev/null +++ b/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp @@ -0,0 +1,15 @@ +// Copyright (C) 2010 by Matthias Troyer , +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +#ifndef ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP +#define ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP +#include +namespace alps { namespace python { + + template void save_observable_to_hdf5(Obs const & obs, std::string const & filename) { + hdf5::archive ar(filename, "a"); + ar["/simulation/results/"+obs.representation()] << obs; + } + +} } // end namespace alps::python +#endif // ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP diff --git a/lib/pyalps/__init__.py b/bindings/python/pyalps/src/pyalps/__init__.py similarity index 73% rename from lib/pyalps/__init__.py rename to bindings/python/pyalps/src/pyalps/__init__.py index 353472049..f4b5c162b 100644 --- a/lib/pyalps/__init__.py +++ b/bindings/python/pyalps/src/pyalps/__init__.py @@ -26,3 +26,12 @@ from .pytools import * from .floatwitherror import FloatWithError from . import fit_wrapper + +# Optional solver modules are present when the wheel was built from an ALPS +# checkout with application bindings enabled. +try: + from ._ext import cthyb, ctint + sys.modules[__name__ + ".cthyb"] = cthyb + sys.modules[__name__ + ".ctint"] = ctint +except ImportError: + pass diff --git a/bindings/python/pyalps/src/pyalps/_ext/__init__.py b/bindings/python/pyalps/src/pyalps/_ext/__init__.py new file mode 100644 index 000000000..f3bf869bc --- /dev/null +++ b/bindings/python/pyalps/src/pyalps/_ext/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +"""Compiled nanobind extensions for pyalps.""" diff --git a/lib/pyalps/alea.py b/bindings/python/pyalps/src/pyalps/alea.py similarity index 100% rename from lib/pyalps/alea.py rename to bindings/python/pyalps/src/pyalps/alea.py diff --git a/lib/pyalps/alea_detail.py b/bindings/python/pyalps/src/pyalps/alea_detail.py similarity index 100% rename from lib/pyalps/alea_detail.py rename to bindings/python/pyalps/src/pyalps/alea_detail.py diff --git a/lib/pyalps/apptest.py b/bindings/python/pyalps/src/pyalps/apptest.py similarity index 100% rename from lib/pyalps/apptest.py rename to bindings/python/pyalps/src/pyalps/apptest.py diff --git a/lib/pyalps/cxx.py b/bindings/python/pyalps/src/pyalps/cxx.py similarity index 68% rename from lib/pyalps/cxx.py rename to bindings/python/pyalps/src/pyalps/cxx.py index 29de94c0f..72784f3e2 100644 --- a/lib/pyalps/cxx.py +++ b/bindings/python/pyalps/src/pyalps/cxx.py @@ -18,28 +18,30 @@ ## while testing (absolute modules, available via PYTHONPATH) try: - from . import pyalea_c - from . import pymcdata_c - from . import pyngsapi_c - from . import pyngsbase_c - from . import pyngshdf5_c - from . import pyngsobservable_c - from . import pyngsobservables_c - from . import pyngsparams_c - from . import pyngsrandom01_c - from . import pyngsresult_c - from . import pyngsresults_c - from . import pytools_c + from ._ext import pyalea_c + from ._ext import pymcdata_c + from ._ext import pyngsbase_c + from ._ext import pyngsapi_c + from ._ext import pyngshdf5_c + from ._ext import pyngsobservable_c + from ._ext import pyngsobservables_c + from ._ext import pyngsparams_c + from ._ext import pyngsrandom01_c + from ._ext import pyngsaccumulator_c + from ._ext import pyngsresult_c + from ._ext import pyngsresults_c + from ._ext import pytools_c except ImportError: import pyalea_c import pymcdata_c - import pyngsapi_c import pyngsbase_c + import pyngsapi_c import pyngshdf5_c import pyngsobservable_c import pyngsobservables_c import pyngsparams_c import pyngsrandom01_c + import pyngsaccumulator_c import pyngsresult_c import pyngsresults_c import pytools_c diff --git a/lib/pyalps/dataset.py b/bindings/python/pyalps/src/pyalps/dataset.py similarity index 100% rename from lib/pyalps/dataset.py rename to bindings/python/pyalps/src/pyalps/dataset.py diff --git a/lib/pyalps/dict_intersect.py b/bindings/python/pyalps/src/pyalps/dict_intersect.py similarity index 100% rename from lib/pyalps/dict_intersect.py rename to bindings/python/pyalps/src/pyalps/dict_intersect.py diff --git a/lib/pyalps/fit_wrapper.py b/bindings/python/pyalps/src/pyalps/fit_wrapper.py similarity index 100% rename from lib/pyalps/fit_wrapper.py rename to bindings/python/pyalps/src/pyalps/fit_wrapper.py diff --git a/lib/pyalps/floatwitherror.py b/bindings/python/pyalps/src/pyalps/floatwitherror.py similarity index 100% rename from lib/pyalps/floatwitherror.py rename to bindings/python/pyalps/src/pyalps/floatwitherror.py diff --git a/lib/pyalps/hdf5.py b/bindings/python/pyalps/src/pyalps/hdf5.py similarity index 100% rename from lib/pyalps/hdf5.py rename to bindings/python/pyalps/src/pyalps/hdf5.py diff --git a/lib/pyalps/hlist.py b/bindings/python/pyalps/src/pyalps/hlist.py similarity index 100% rename from lib/pyalps/hlist.py rename to bindings/python/pyalps/src/pyalps/hlist.py diff --git a/lib/pyalps/lattice.py b/bindings/python/pyalps/src/pyalps/lattice.py similarity index 100% rename from lib/pyalps/lattice.py rename to bindings/python/pyalps/src/pyalps/lattice.py diff --git a/lib/pyalps/load.py b/bindings/python/pyalps/src/pyalps/load.py similarity index 100% rename from lib/pyalps/load.py rename to bindings/python/pyalps/src/pyalps/load.py diff --git a/lib/pyalps/math.py b/bindings/python/pyalps/src/pyalps/math.py similarity index 100% rename from lib/pyalps/math.py rename to bindings/python/pyalps/src/pyalps/math.py diff --git a/lib/pyalps/maxent.py b/bindings/python/pyalps/src/pyalps/maxent.py similarity index 91% rename from lib/pyalps/maxent.py rename to bindings/python/pyalps/src/pyalps/maxent.py index 60fd9c9bf..ead5da1fe 100644 --- a/lib/pyalps/maxent.py +++ b/bindings/python/pyalps/src/pyalps/maxent.py @@ -11,8 +11,4 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -try: - from .maxent_c import * -except ImportError: - from maxent_c import * - \ No newline at end of file +from ._ext.maxent_c import * diff --git a/lib/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py similarity index 100% rename from lib/pyalps/mpi.py rename to bindings/python/pyalps/src/pyalps/mpi.py diff --git a/lib/pyalps/mpl_setup_macosx.py b/bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py similarity index 100% rename from lib/pyalps/mpl_setup_macosx.py rename to bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py diff --git a/lib/pyalps/mpl_setup_qt.py b/bindings/python/pyalps/src/pyalps/mpl_setup_qt.py similarity index 100% rename from lib/pyalps/mpl_setup_qt.py rename to bindings/python/pyalps/src/pyalps/mpl_setup_qt.py diff --git a/lib/pyalps/mpl_setup_tk.py b/bindings/python/pyalps/src/pyalps/mpl_setup_tk.py similarity index 100% rename from lib/pyalps/mpl_setup_tk.py rename to bindings/python/pyalps/src/pyalps/mpl_setup_tk.py diff --git a/lib/pyalps/natural_sort.py b/bindings/python/pyalps/src/pyalps/natural_sort.py similarity index 100% rename from lib/pyalps/natural_sort.py rename to bindings/python/pyalps/src/pyalps/natural_sort.py diff --git a/lib/pyalps/ngs.py b/bindings/python/pyalps/src/pyalps/ngs.py similarity index 74% rename from lib/pyalps/ngs.py rename to bindings/python/pyalps/src/pyalps/ngs.py index 097d7aaa0..cc3922687 100644 --- a/lib/pyalps/ngs.py +++ b/bindings/python/pyalps/src/pyalps/ngs.py @@ -18,16 +18,13 @@ from collections.abc import MutableMapping else: from collections import MutableMapping -import types - from .cxx.pyngsparams_c import params -params.__bases__ = (MutableMapping, ) + params.__bases__ from .cxx.pyngsobservable_c import observable -class ObservableOperators: - def __lshift__(self, other): - self.append(other) -observable.__bases__ = (ObservableOperators, ) + observable.__bases__ +def _observable_lshift(self, other): + self.append(other) + return self +observable.__lshift__ = _observable_lshift class RealObservable: def __init__(self, name, binnum = 0): @@ -44,7 +41,6 @@ def addToObservables(self, observables): #rename this with new ALEA observables.createRealVectorObservable(self.name, self.binnum) from .cxx.pyngsobservables_c import observables -observables.__bases__ = (MutableMapping, ) + observables.__bases__ from .cxx.pyngsobservable_c import createRealObservable #remove this with new ALEA! from .cxx.pyngsobservable_c import createRealVectorObservable #remove this with new ALEA! @@ -53,7 +49,17 @@ def addToObservables(self, observables): #rename this with new ALEA from .cxx.pyngsresult_c import observable2result #remove this with new ALEA! from .cxx.pyngsresults_c import results -results.__bases__ = (MutableMapping, ) + results.__bases__ + +# Boost.Python allowed mutating extension-type base classes after creation. +# nanobind extension types use a different allocator/deallocator layout, so +# register them as virtual MutableMapping implementations and copy the mixin +# methods onto the concrete classes instead. +for _mapping_type in (params, observables, results): + MutableMapping.register(_mapping_type) + for _method in ("keys", "values", "items", "get", "pop", "popitem", + "clear", "update", "setdefault", "__eq__", "__ne__"): + if not hasattr(_mapping_type, _method): + setattr(_mapping_type, _method, getattr(MutableMapping, _method)) from .cxx.pyngsbase_c import mcbase diff --git a/lib/pyalps/plot.py b/bindings/python/pyalps/src/pyalps/plot.py similarity index 100% rename from lib/pyalps/plot.py rename to bindings/python/pyalps/src/pyalps/plot.py diff --git a/lib/pyalps/plot_core.py b/bindings/python/pyalps/src/pyalps/plot_core.py similarity index 100% rename from lib/pyalps/plot_core.py rename to bindings/python/pyalps/src/pyalps/plot_core.py diff --git a/lib/pyalps/pyalps_config.py b/bindings/python/pyalps/src/pyalps/pyalps_config.py similarity index 100% rename from lib/pyalps/pyalps_config.py rename to bindings/python/pyalps/src/pyalps/pyalps_config.py diff --git a/lib/pyalps/pyalps_config.py.in b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in similarity index 51% rename from lib/pyalps/pyalps_config.py.in rename to bindings/python/pyalps/src/pyalps/pyalps_config.py.in index 599df9a7f..150ef0f5e 100644 --- a/lib/pyalps/pyalps_config.py.in +++ b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in @@ -1,2 +1,2 @@ ALPS_XML_INSTALL_DIR="@CMAKE_INSTALL_PREFIX@/lib/xml" -ALPS_BIN_INSTALL_DIR="@CMAKE_INSTALL_PREFIX@/bin" \ No newline at end of file +ALPS_BIN_INSTALL_DIR="@CMAKE_INSTALL_PREFIX@/bin" diff --git a/lib/pyalps/pytools.py b/bindings/python/pyalps/src/pyalps/pytools.py similarity index 100% rename from lib/pyalps/pytools.py rename to bindings/python/pyalps/src/pyalps/pytools.py diff --git a/lib/pyalps/tools.py b/bindings/python/pyalps/src/pyalps/tools.py similarity index 100% rename from lib/pyalps/tools.py rename to bindings/python/pyalps/src/pyalps/tools.py diff --git a/lib/pyalps/CMakeLists.txt b/lib/pyalps/CMakeLists.txt deleted file mode 100644 index 1df8d203d..000000000 --- a/lib/pyalps/CMakeLists.txt +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright Matthias Troyer 2009 - 2010. -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the “Software”), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# -# python exports -# - -set(ALPS_SHARED_CPPFLAGS PYALPS_EXPORTS=1) -set(ALPS_STATIC_CPPFLAGS "") - -if (ALPS_HAVE_PYTHON AND NOT ALPS_BUILD_LIBS_ONLY) - - set(OLD_SHARED ${BUILD_SHARED_LIBS}) - set(BUILD_SHARED_LIBS ON) - set(PYALEA_SOURCES ../../src/alps/python/pyalea.cpp ) - set(PYMCDATA_SOURCES ../../src/alps/python/pymcdata.cpp ) - set(PYTOOLS_SOURCES ../../src/alps/python/pytools.cpp) - - set(PYALPS_SOURCES pyalea_c pymcdata_c pytools_c pyngsparams_c pyngshdf5_c pyngsbase_c - pyngsobservable_c pyngsobservables_c pyngsresult_c pyngsresults_c pyngsapi_c pyngsrandom01_c - ) - - if(ALPS_NGS_USE_NEW_ALEA) - list(APPEND PYALPS_SOURCES pyngsaccumulator_c) - endif(ALPS_NGS_USE_NEW_ALEA) - - set (MAXENT_SOURCES ../../tool/maxent.cpp ../../tool/maxent_helper.cpp - ../../tool/maxent_simulation.cpp ../../tool/maxent_parms.cpp) - - set (CTHYB_SOURCES ../../applications/dmft/qmc/hybridization/hybmain.cpp - ../../applications/dmft/qmc/hybridization/hybsim.cpp - ../../applications/dmft/qmc/hybridization/hyblocal.cpp - ../../applications/dmft/qmc/hybridization/hybint.cpp - ../../applications/dmft/qmc/hybridization/hybfun.cpp - ../../applications/dmft/qmc/hybridization/hybretintfun.cpp - ../../applications/dmft/qmc/hybridization/hybmatrix.cpp - ../../applications/dmft/qmc/hybridization/hybmatrix_ft.cpp - ../../applications/dmft/qmc/hybridization/hybconfig.cpp - ../../applications/dmft/qmc/hybridization/hybupdates.cpp - ../../applications/dmft/qmc/hybridization/hybevaluate.cpp - ../../applications/dmft/qmc/hybridization/hybmeasurements.cpp) - set (CTINT_SOURCES ../../applications/dmft/qmc/interaction_expansion2/main.cpp - ../../applications/dmft/qmc/fouriertransform.C - ../../applications/dmft/qmc/interaction_expansion2/auxiliary.cpp - ../../applications/dmft/qmc/interaction_expansion2/observables.cpp - ../../applications/dmft/qmc/interaction_expansion2/fastupdate.cpp - ../../applications/dmft/qmc/interaction_expansion2/selfenergy.cpp - ../../applications/dmft/qmc/interaction_expansion2/solver.cpp - ../../applications/dmft/qmc/interaction_expansion2/io.cpp - ../../applications/dmft/qmc/interaction_expansion2/splines.cpp - ../../applications/dmft/qmc/interaction_expansion2/interaction_expansion.cpp - ../../applications/dmft/qmc/interaction_expansion2/measurements.cpp - ../../applications/dmft/qmc/interaction_expansion2/model.cpp) - - set(PYNGSPARAMS_SOURCES ../../src/alps/ngs/python/params.cpp) - set(PYNGSHDF5_SOURCES ../../src/alps/ngs/python/hdf5.cpp) - set(PYNGSBASE_SOURCES ../../src/alps/ngs/python/mcbase.cpp) - set(PYNGSOBSERVABLE_SOURCES ../../src/alps/ngs/python/observable.cpp) - set(PYNGSOBSERVABLES_SOURCES ../../src/alps/ngs/python/observables.cpp) - set(PYNGSRESULT_SOURCES ../../src/alps/ngs/python/result.cpp) - set(PYNGSRESULTS_SOURCES ../../src/alps/ngs/python/results.cpp) - set(PYNGSAPI_SOURCES ../../src/alps/ngs/python/api.cpp) - set(PYNGSRANDOM01_SOURCES ../../src/alps/ngs/python/random01.cpp) - set(PYNGSACCUMULATOR_SOURCES ../../src/alps/ngs/python/accumulator.cpp) - - if(LAPACK_FOUND AND ALPS_BUILD_APPLICATIONS) - set(PYALPS_SOURCES ${PYALPS_SOURCES} maxent_c cthyb ctint) - python_add_module(maxent_c ${MAXENT_SOURCES}) - python_add_module(cthyb ${CTHYB_SOURCES}) - python_add_module(ctint ${CTINT_SOURCES}) - include_directories(../../applications/dmft/qmc) - set_target_properties(maxent_c PROPERTIES COMPILE_FLAGS "-DBUILD_PYTHON_MODULE") - set_target_properties(cthyb PROPERTIES COMPILE_FLAGS "-DBUILD_PYTHON_MODULE") - set_target_properties(ctint PROPERTIES COMPILE_FLAGS "-DBUILD_PYTHON_MODULE") - endif(LAPACK_FOUND AND ALPS_BUILD_APPLICATIONS) - - python_add_module(pyalea_c ${PYALEA_SOURCES}) - python_add_module(pymcdata_c ${PYMCDATA_SOURCES}) - python_add_module(pytools_c ${PYTOOLS_SOURCES}) - python_add_module(pyngsparams_c ${PYNGSPARAMS_SOURCES}) - python_add_module(pyngshdf5_c ${PYNGSHDF5_SOURCES}) - python_add_module(pyngsbase_c ${PYNGSBASE_SOURCES}) - python_add_module(pyngsobservable_c ${PYNGSOBSERVABLE_SOURCES}) - python_add_module(pyngsobservables_c ${PYNGSOBSERVABLES_SOURCES}) - python_add_module(pyngsresult_c ${PYNGSRESULT_SOURCES}) - python_add_module(pyngsresults_c ${PYNGSRESULTS_SOURCES}) - python_add_module(pyngsapi_c ${PYNGSAPI_SOURCES}) - python_add_module(pyngsrandom01_c ${PYNGSRANDOM01_SOURCES}) - - if(ALPS_NGS_USE_NEW_ALEA) - python_add_module(pyngsaccumulator_c ${PYNGSACCUMULATOR_SOURCES}) - endif(ALPS_NGS_USE_NEW_ALEA) - - - - FOREACH (name ${PYALPS_SOURCES}) - if(BUILD_SHARED_LIBS) - set_target_properties(${name} PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - set_target_properties(${name} PROPERTIES PREFIX "") - if(WIN32 AND NOT UNIX) - set_target_properties(${name} PROPERTIES SUFFIX ".pyd") - endif(WIN32 AND NOT UNIX) - endif(BUILD_SHARED_LIBS) - target_link_libraries(${name} ${LINK_LIBRARIES} ${BLAS_LIBRARY} ${LAPACK_LIBRARY} ${LAPACK_LINKER_FLAGS}) - if(ALPS_PYTHON_WHEEL) - target_link_libraries(${name} alps_python) - if(APPLE) - set_target_properties(${name} PROPERTIES INSTALL_RPATH "@loader_path/lib" ) - else(APPLE) - set_target_properties(${name} PROPERTIES INSTALL_RPATH "$ORIGIN/lib" ) - endif(APPLE) - else() - target_link_libraries(${name} alps) - if(APPLE) - set_target_properties(${name} PROPERTIES INSTALL_RPATH "@loader_path/../../.." ) - else(APPLE) - set_target_properties(${name} PROPERTIES INSTALL_RPATH "$ORIGIN/../../.." ) - endif(APPLE) - endif() - ENDFOREACH(name) - - ####################################################################### - # install - ####################################################################### - if(NOT ALPS_PYTHON_WHEEL) - install(TARGETS ${PYALPS_SOURCES} - COMPONENT python - RUNTIME DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps/bin - ARCHIVE DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps - LIBRARY DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps) - else(NOT ALPS_PYTHON_WHEEL) - install(TARGETS ${PYALPS_SOURCES} - COMPONENT python - RUNTIME DESTINATION pyalps/bin - ARCHIVE DESTINATION pyalps - LIBRARY DESTINATION pyalps) - endif(NOT ALPS_PYTHON_WHEEL) - set(BUILD_SHARED_LIBS ${OLD_SHARED}) -endif (ALPS_HAVE_PYTHON AND NOT ALPS_BUILD_LIBS_ONLY) diff --git a/pyproject.toml b/pyproject.toml index 17faed7e6..c8a0571c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,85 +1,23 @@ [build-system] -requires = ["scikit-build-core", "numpy", "scipy"] +requires = ["scikit-build-core>=0.10", "nanobind>=2.10"] build-backend = "scikit_build_core.build" -[tool.scikit-build] -wheel.packages = ["python/pyalps"] -build.verbose = true -#build-dir = "./build_alps" -#build.tool-args = ["-j8", "-l13"] -logging.level = "DEBUG" - -[tool.scikit-build.cmake.define] -Boost_SRC_DIR = {env="Boost_SRC_DIR"} -CMAKE_CXX_FLAGS = "-fPIC -fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" -CMAKE_C_FLAGS = "-fPIC" -ALPS_PYTHON_WHEEL = "ON" -ALPS_BUILD_FORTRAN = "ON" - -[[tool.scikit-build.overrides]] -if.platform-system = "^darwin" -cmake.define.CMAKE_CXX_FLAGS = "-fPIC -stdlib=libc++ -fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" -cmake.define.Boost_SRC_DIR = {env="Boost_SRC_DIR"} -cmake.define.ALPS_BUILD_FORTRAN = "ON" -cmake.define.CMAKE_C_FLAGS = "-fPIC" -cmake.define.ALPS_PYTHON_WHEEL = "ON" - [project] name = "pyalps" version = "2.3.4b1" -authors = [ - { name="Sergei Iskakov", email="siskakov@umich.edu" }, - { name="Fei Lin", email="feilin.physics@gmail.com" } -] -license = {text = "MIT License"} - -dependencies = ["numpy", "scipy"] - description = "Python Applications and Libraries for Physics Simulations" -readme = "README-py.md" +readme = "bindings/python/pyalps/README.md" requires-python = ">=3.9" -classifiers = [ - "Development Status :: 5 - Production/Stable", - 'Intended Audience :: Science/Research', - 'Intended Audience :: Developers', - 'Programming Language :: C++', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: Implementation :: CPython', - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: MacOS", -] - -[project.urls] -Homepage = "https://alps.comp-phys.com" -Issues = "https://github.com/ALPSim/ALPS/issues" +license = "MIT" +dependencies = ["numpy", "scipy"] +[tool.scikit-build] +cmake.source-dir = "bindings/python/pyalps" +wheel.packages = ["bindings/python/pyalps/src/pyalps"] +build.verbose = true -[project.optional-dependencies] -tests = [ - 'coverage>=5.0.3', - 'pytest', - 'pytest-benchmark[histogram]>=3.2.1', -] +[tool.scikit-build.cmake.define] +ALPS_DIR = { env = "ALPS_DIR" } [tool.cibuildwheel] -skip = ["*-musllinux*"] -test-requires = "pytest" -test-command = "pytest {project}/test" manylinux-x86_64-image = "manylinux_2_28" - -[tool.cibuildwheel.linux] -before-all = "dnf install -y epel-release; dnf config-manager --set-enabled powertools; dnf install -y fftw-devel hdf5-devel openblas-devel wget; pipx install patchelf==0.14.5.0 --force" -#before-build="" -test-command = "pytest {project}/test/pyalps" - -[tool.cibuildwheel.macos] -before-all = "brew reinstall hdf5 fftw gfortran" -test-command = "pytest {project}/test/pyalps" diff --git a/src/alps/CMakeLists.txt b/src/alps/CMakeLists.txt index 9c495dc88..462986ae3 100644 --- a/src/alps/CMakeLists.txt +++ b/src/alps/CMakeLists.txt @@ -22,7 +22,6 @@ # set(ALPS_SOURCES "") -set(ALPS_PYTHON_SOURCES "") set(ALPS_SHARED_CPPFLAGS ALPS_EXPORTS=1) set(ALPS_STATIC_CPPFLAGS "") @@ -74,10 +73,6 @@ set(ALPS_SOURCES set(ALPS_SOURCES ${ALPS_SOURCES} osiris/xdr.c osiris/xdr_array.c osiris/xdr_float.c osiris/xdr_stdio.c) endif (NOT ALPS_HAVE_RPC_XDR_H) - if (ALPS_BUILD_PYTHON) - set(ALPS_PYTHON_SOURCES ${ALPS_PYTHON_SOURCES} ngs/lib/get_numpy_type.cpp hdf5/python.cpp python/numpy_array.cpp) - endif (ALPS_BUILD_PYTHON) - # OpenMPI ULFM if (ALPS_NGS_OPENMPI_ULFM) set(ALPS_SOURCES ${ALPS_SOURCES} ngs/lib/ulfm.cpp) @@ -100,34 +95,12 @@ set(ALPS_SOURCES ${ALPS_SOURCES} ngs/lib/clone.cpp ngs/lib/clone_info.cpp ngs/lib/job.cpp ngs/lib/parapack.cpp ngs/lib/worker_factory.cpp ) -if(ALPS_HAVE_PYTHON) - if(ALPS_PYTHON_WHEEL) - set(ALPS_PYTHON_SOURCES ${ALPS_PYTHON_SOURCES} ${ALPS_SOURCES}) - add_library(alps_python ${ALPS_PYTHON_SOURCES}) - target_compile_definitions(alps_python PRIVATE ALPS_HAVE_PYTHON) - get_target_property(XXX alps_python COMPILE_DEFINITIONS) - message(STATUS "ALPS_PYTHON: ${XXX}") - if(ALPS_HAVE_BOOST_NUMPY) - target_compile_definitions(alps_python INTERFACE ALPS_HAVE_BOOST_NUMPY) - endif() - else() - set(ALPS_SOURCES ${ALPS_PYTHON_SOURCES} ${ALPS_SOURCES}) - endif() -endif() - add_library(alps ${ALPS_SOURCES}) find_package(HDF5) if (Boost_FOUND) # link to ${Boost_LIBRARIES} when precompiled Boost libraries found set(ALPS_LINK_LIBS ${Boost_LIBRARIES} ${HDF5_LIBRARIES}) # ${SZIP_LIBRARIES}) - if(PYTHONLIBS_FOUND) - if(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - set(ALPS_LINK_LIBS ${ALPS_LINK_LIBS} ${PYTHON_LIBRARY}) # ${PYTHON_EXTRA_LIBS}) - else(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - set(ALPS_LINK_LIBS ${ALPS_LINK_LIBS} ${PYTHON_LIBRARY} ${PYTHON_EXTRA_LIBS}) - endif(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - endif(PYTHONLIBS_FOUND) if(MPI_FOUND) set(ALPS_LINK_LIBS ${ALPS_LINK_LIBS} ${MPI_LIBRARIES}) if(MPI_EXTRA_LIBRARY) @@ -138,13 +111,6 @@ if (Boost_FOUND) else (Boost_FOUND) # "boost" target available when Boost libraries are built from source target_link_libraries(alps ${ALPS_BOOST_LIBRARY_NAME} ${HDF5_LIBRARIES}) # ${SZIP_LIBRARIES}) - if(ALPS_HAVE_PYTHON) - if(ALPS_PYTHON_WHEEL) - target_link_libraries(alps_python ${ALPS_BOOST_LIBRARY_NAME} ${ALPS_BOOST_PYTHON_LIBRARY_NAME} ${HDF5_LIBRARIES}) # ${SZIP_LIBRARIES}) - else() - target_link_libraries(alps ${ALPS_BOOST_PYTHON_LIBRARY_NAME}) # ${SZIP_LIBRARIES}) - endif() - endif() endif (Boost_FOUND) if(BUILD_SHARED_LIBS) @@ -156,25 +122,8 @@ if(BUILD_SHARED_LIBS) endif(HDF5_DEFINITIONS) set_target_properties(alps PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - if(ALPS_HAVE_PYTHON) - if(ALPS_PYTHON_WHEEL) - set_target_properties(alps_python PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - target_compile_definitions(alps_python PUBLIC ALPS_HAVE_PYTHON) - if(ALPS_HAVE_BOOST_NUMPY) - target_compile_definitions(alps_python PUBLIC ALPS_HAVE_BOOST_NUMPY) - endif() - else(ALPS_PYTHON_WHEEL) - target_compile_definitions(alps PUBLIC ALPS_HAVE_PYTHON) - if(ALPS_HAVE_BOOST_NUMPY) - target_compile_definitions(alps PUBLIC ALPS_HAVE_BOOST_NUMPY) - endif() - endif(ALPS_PYTHON_WHEEL) - endif() else(BUILD_SHARED_LIBS) set_target_properties(alps PROPERTIES COMPILE_DEFINITIONS "${ALPS_STATIC_CPPFLAGS}") - if(ALPS_HAVE_PYTHON) - set_target_properties(alps_python PROPERTIES COMPILE_DEFINITIONS "${ALPS_STATIC_CPPFLAGS}") - endif() endif(BUILD_SHARED_LIBS) @@ -185,12 +134,12 @@ if(MSVC) endif(MSVC) # Set soversion for library -if(NOT WIN32 AND NOT APPLE AND NOT ALPS_PYTHON_WHEEL) +if(NOT WIN32 AND NOT APPLE) set_target_properties(alps PROPERTIES SOVERSION "${ALPS_VERSION_MAJOR}" VERSION "${ALPS_VERSION_MAJOR}.${ALPS_VERSION_MINOR}.${ALPS_VERSION_PATCH}" ) -endif(NOT WIN32 AND NOT APPLE AND NOT ALPS_PYTHON_WHEEL) +endif(NOT WIN32 AND NOT APPLE) #boost librt linking @@ -207,7 +156,6 @@ endif() ####################################################################### # install ####################################################################### -if(NOT ALPS_PYTHON_WHEEL) install(TARGETS alps COMPONENT libraries ARCHIVE DESTINATION lib LIBRARY DESTINATION lib @@ -219,13 +167,3 @@ if(ALPS_BUILD_FORTRAN) LIBRARY DESTINATION lib RUNTIME DESTINATION bin) endif(ALPS_BUILD_FORTRAN) -else () - install(TARGETS alps_python COMPONENT libraries - ARCHIVE DESTINATION pyalps/lib - LIBRARY DESTINATION pyalps/lib - RUNTIME DESTINATION pyalps/bin) - install(TARGETS alps COMPONENT libraries - ARCHIVE DESTINATION pyalps/lib - LIBRARY DESTINATION pyalps/lib - RUNTIME DESTINATION pyalps/bin) -endif() diff --git a/src/alps/alea/mcanalyze.hpp b/src/alps/alea/mcanalyze.hpp index 1dbdc989a..f4b1bd99d 100644 --- a/src/alps/alea/mcanalyze.hpp +++ b/src/alps/alea/mcanalyze.hpp @@ -646,4 +646,3 @@ ALPS_MCANALYZE_IMPLEMENT_OSTREAM(mctimeseries_view) #endif - diff --git a/src/alps/ngs/numeric/vector.hpp b/src/alps/ngs/numeric/vector.hpp index c57ac23ac..f6d9cc2fa 100644 --- a/src/alps/ngs/numeric/vector.hpp +++ b/src/alps/ngs/numeric/vector.hpp @@ -80,36 +80,12 @@ namespace alps { using boost::numeric::operators::operator+; return lhs + rhs; } - //------------------- operator + with scalar ------------------- - template - std::vector operator + (std::vector arg, T const & scalar) { - std::transform(arg.begin(), arg.end(), arg.begin(), boost::lambda::_1 + scalar); - return arg; - } - template - std::vector operator + (T const & scalar, std::vector arg) { - std::transform(arg.begin(), arg.end(), arg.begin(), scalar + boost::lambda::_1); - return arg; - } - //------------------- operator - ------------------- template std::vector operator - (std::vector const & lhs, std::vector const & rhs) { using boost::numeric::operators::operator-; return lhs - rhs; } - //------------------- operator + with scalar ------------------- - template - std::vector operator - (std::vector arg, T const & scalar) { - std::transform(arg.begin(), arg.end(), arg.begin(), boost::lambda::_1 + scalar); - return arg; - } - template - std::vector operator - (T const & scalar, std::vector arg) { - std::transform(arg.begin(), arg.end(), arg.begin(), scalar + boost::lambda::_1); - return arg; - } - //------------------- operator * vector-vector------------------- template std::vector operator * (std::vector const & lhs, std::vector const & rhs) { @@ -126,12 +102,14 @@ namespace alps { //------------------- operator + with scalar ------------------- template std::vector operator + (T const & scalar, std::vector lhs) { - std::transform(lhs.begin(), lhs.end(), lhs.begin(), bind1st(std::plus(), scalar)); + std::transform(lhs.begin(), lhs.end(), lhs.begin(), + [scalar](T const & value) { return scalar + value; }); return lhs; } template std::vector operator + (std::vector lhs, T const & scalar) { - std::transform(lhs.begin(), lhs.end(), lhs.begin(), bind2nd(std::plus(), scalar)); + std::transform(lhs.begin(), lhs.end(), lhs.begin(), + [scalar](T const & value) { return value + scalar; }); return lhs; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bc1c49038..177b33c49 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -16,7 +16,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -if(NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if(NOT ALPS_BUILD_LIBS_ONLY) add_subdirectory(accumulator) add_subdirectory(alea) add_subdirectory(fixed_capacity) @@ -30,7 +30,6 @@ add_subdirectory(osiris) add_subdirectory(parameter) add_subdirectory(parapack) add_subdirectory(parser) -add_subdirectory(pyalps) add_subdirectory(random) add_subdirectory(utility) -endif() \ No newline at end of file +endif() diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py new file mode 100644 index 000000000..fd62e4063 --- /dev/null +++ b/test/pyalps/test_binding_surface.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +"""Lock the public pyalps extension surface after the nanobind migration.""" + +from __future__ import annotations + +import copy +import importlib +import os +import tempfile + +import numpy as np + + +def test_extension_import_surface(): + import pyalps + import pyalps.cxx as cxx + + expected = { + "pyalea_c", + "pymcdata_c", + "pytools_c", + "pyngsparams_c", + "pyngshdf5_c", + "pyngsbase_c", + "pyngsobservable_c", + "pyngsobservables_c", + "pyngsresult_c", + "pyngsresults_c", + "pyngsapi_c", + "pyngsrandom01_c", + "pyngsaccumulator_c", + } + assert pyalps is not None + assert expected <= set(vars(cxx)) + + +def test_cross_module_parameter_archive_and_rng_roundtrip(): + from pyalps.cxx import pyngshdf5_c, pyngsparams_c, pyngsrandom01_c + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "surface.h5") + params = pyngsparams_c.params() + params["integer"] = 42 + params["real"] = 3.25 + params["flag"] = True + params["text"] = "nanobind-lock" + + rng = pyngsrandom01_c.random01(91) + for _ in range(7): + rng() + + archive = pyngshdf5_c.hdf5_archive_impl(path, "w") + archive.create_group("/parameters") + archive.set_context("/parameters") + params.save(archive) + archive.set_context("/") + rng.save(archive) + del archive + + loaded = pyngsparams_c.params() + restored_rng = pyngsrandom01_c.random01(0) + archive = pyngshdf5_c.hdf5_archive_impl(path, "r") + archive.set_context("/parameters") + loaded.load(archive) + archive.set_context("/") + restored_rng.load(archive) + del archive + + assert sorted(loaded) == sorted(params) + assert int(loaded["integer"]) == 42 + assert float(loaded["real"]) == 3.25 + assert bool(loaded["flag"]) is True + assert str(loaded["text"]) == "nanobind-lock" + assert [rng() for _ in range(5)] == [restored_rng() for _ in range(5)] + + +def test_alea_numpy_and_mcdata_operators(): + from pyalps.cxx.pyalea_c import MCScalarTimeseries, RealObservable, mean, size + from pyalps.cxx.pymcdata_c import MCScalarData + + observable = RealObservable("energy") + for sample in (0.9, 1.0, 1.1, 1.0): + observable << sample + assert observable.count == 4 + assert abs(observable.mean - 1.0) < 1e-12 + assert observable.error >= 0 + + series = MCScalarTimeseries(np.asarray([1.0, 2.0, 3.0])) + assert size(series) == 3 + assert mean(series) == 2.0 + np.testing.assert_allclose(series.timeseries(), [1.0, 2.0, 3.0]) + + first = MCScalarData(1.0, 0.1) + second = MCScalarData(2.0, 0.2) + total = first + second + assert total.mean == 3.0 + assert total.error > 0 + duplicate = copy.deepcopy(total) + assert duplicate.mean == total.mean + assert duplicate.error == total.error + + +def test_ngs_observable_containers(): + from pyalps import ngs + + observables = ngs.observables() + observables.createRealObservable("magnetization") + observables["magnetization"] << 1.5 + assert "magnetization" in observables + assert ngs.observable2result(observables["magnetization"]).count == 1 + + +def test_name_encoding_roundtrip(): + from pyalps.cxx.pytools_c import hdf5_name_decode, hdf5_name_encode + + for value in ("plain", "with space", "slash/inside", "café"): + assert hdf5_name_decode(hdf5_name_encode(value)) == value + + +def test_accumulator_surface(): + from pyalps.cxx.pyngsaccumulator_c import error_accumulator + + accumulator = error_accumulator() + for sample in (1.0, 2.0, 3.0): + accumulator(sample) + result = accumulator.result() + assert result.count() == 3 + assert result.mean() == 2.0 + assert result.error() >= 0 + + +def test_optional_application_extension_surface(): + for name in ("maxent_c", "dwa_c", "cthyb", "ctint"): + module = importlib.import_module("pyalps._ext." + name) + assert module.__name__.endswith(name) + + +if __name__ == "__main__": + for test in ( + test_extension_import_surface, + test_cross_module_parameter_archive_and_rng_roundtrip, + test_alea_numpy_and_mcdata_operators, + test_ngs_observable_containers, + test_name_encoding_roundtrip, + test_accumulator_surface, + test_optional_application_extension_surface, + ): + test() + print("pyalps binding surface: green") diff --git a/tool/maxent.cpp b/tool/maxent.cpp index ae5b4ac21..ce0a4aeac 100644 --- a/tool/maxent.cpp +++ b/tool/maxent.cpp @@ -45,11 +45,11 @@ bool stop_callback(boost::posix_time::ptime const & end_time) { #ifdef BUILD_PYTHON_MODULE -//compile it as a python module (requires boost::python library) -using namespace boost::python; +#include "dict_to_params.hpp" +namespace nb = nanobind; -void run_it(boost::python::dict parms_){ - alps::parameters_type::type parms(parms_); +void run_it(nb::dict const & parms_){ + alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string out_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else @@ -90,9 +90,7 @@ void run_it(boost::python::dict parms_){ } #ifdef BUILD_PYTHON_MODULE - BOOST_PYTHON_MODULE(maxent_c) - { - def("AnalyticContinuation",run_it);//define python-callable run method - }; + NB_MODULE(maxent_c, m) { + m.def("AnalyticContinuation", run_it); + } #endif - diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index d88dc6974..bc38361a8 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -17,7 +17,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -if(NOT ALPS_PYTHON_WHEEL AND NOT ALPS_BUILD_LIBS_ONLY) +if(NOT ALPS_BUILD_LIBS_ONLY) install(DIRECTORY . DESTINATION tutorials COMPONENT tutorials FILES_MATCHING PATTERN "*.py" PATTERN "*.ipynb" PATTERN "*.sh" PATTERN "parm*" PATTERN "*params" PATTERN "*.ip" PATTERN "*.op" PATTERN "*.dat" PATTERN "*.parm" PATTERN "*.xml" PATTERN "*input*" PATTERN "*.pvsm" From 5e4401f88c95a8ab165ebd603c8e6dc8d9e68296 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 13:20:31 -0500 Subject: [PATCH 02/52] build: retire legacy Python CMake paths --- .github/workflows/build_wheels.yml | 15 +- CMakeLists.txt | 1 - applications/diag/fulldiag/CMakeLists.txt | 11 - applications/diag/sparsediag/CMakeLists.txt | 9 - applications/dmft/qmc/CMakeLists.txt | 18 -- .../hybridization/Documentation/hybdoc.tex | 2 +- applications/dmrg/dmrg/CMakeLists.txt | 9 - applications/mc/simple/CMakeLists.txt | 11 +- applications/mc/spins/CMakeLists.txt | 11 - applications/qmc/checksign/CMakeLists.txt | 11 +- applications/qmc/looper/CMakeLists.txt | 9 - applications/qmc/qwl/CMakeLists.txt | 13 +- applications/qmc/sse/CMakeLists.txt | 20 +- applications/qmc/sse4/CMakeLists.txt | 11 +- applications/qmc/worms/CMakeLists.txt | 15 +- bindings/python/pyalps/README.md | 1 + cmake/ALPSConfig.cmake.in | 11 - cmake/FindBoostForALPS.cmake | 87 ------ cmake/FindBoostSrc.cmake | 22 -- cmake/FindPythonMod.cmake | 275 ------------------ cmake/UseALPS.cmake | 9 - src/alps/config.h.in | 10 - src/boost/CMakeLists.txt | 101 +------ test/pyalps/CMakeLists.txt | 47 --- tool/CMakeLists.txt | 27 -- .../code-07-mcmain-mcbase/CMakeLists.txt | 19 -- .../heisenberg/o_n_model/CMakeLists.txt | 20 -- 27 files changed, 22 insertions(+), 773 deletions(-) delete mode 100644 cmake/FindPythonMod.cmake delete mode 100644 test/pyalps/CMakeLists.txt diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 7b7fef3c3..fdf42bc3d 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -21,7 +21,6 @@ jobs: - { os: ubuntu-latest, target: "", arch: x86_64, homebrew: ''} #- { os: macos-13, target: "13.0" , arch: x86_64, homebrew: '/usr/local'} #DEPRECATED. Too old. - { os: macos-15, target: "15.0" , arch: arm64, homebrew: '/opt/homebrew'} - - { os: macos-26, target: "26.0" , arch: arm64, homebrew: '/opt/homebrew'} steps: - uses: actions/checkout@v7 @@ -45,7 +44,7 @@ jobs: CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" CIBW_BEFORE_ALL_LINUX: > - dnf install -y ccache cmake hdf5-devel openmpi-devel lapack-devel ninja-build && + dnf install -y ccache cmake hdf5-devel lapack-devel ninja-build && cmake -S {project} -B {project}/_build/cibw-alps -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install @@ -53,10 +52,11 @@ jobs: -DALPS_BUILD_LIBS_ONLY=ON -DALPS_BUILD_TESTS=OFF -DALPS_BUILD_EXAMPLES=OFF - -DALPS_BUILD_APPLICATIONS=OFF && + -DALPS_BUILD_APPLICATIONS=OFF + -DALPS_ENABLE_MPI=OFF && cmake --build {project}/_build/cibw-alps --target install -j2 CIBW_BEFORE_ALL_MACOS: > - brew install ccache cmake hdf5 open-mpi ninja && + brew install ccache cmake hdf5 ninja && cmake -S {project} -B {project}/_build/cibw-alps -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install @@ -65,6 +65,7 @@ jobs: -DALPS_BUILD_TESTS=OFF -DALPS_BUILD_EXAMPLES=OFF -DALPS_BUILD_APPLICATIONS=OFF + -DALPS_ENABLE_MPI=OFF -DHDF5_ROOT=${{ matrix.plat.homebrew }}/opt/hdf5 && cmake --build {project}/_build/cibw-alps --target install -j2 CIBW_ENVIRONMENT_MACOS: > @@ -74,6 +75,12 @@ jobs: CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }} CXXFLAGS="-stdlib=libc++" + CIBW_REPAIR_WHEEL_COMMAND_LINUX: > + auditwheel repair -w {dest_dir} {wheel} && + auditwheel show {dest_dir}/*.whl + CIBW_REPAIR_WHEEL_COMMAND_MACOS: > + delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && + delocate-listdeps --all {dest_dir}/*.whl CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: pytest -q {project}/test/pyalps diff --git a/CMakeLists.txt b/CMakeLists.txt index 3924a5e2b..5f5b3ff2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -277,7 +277,6 @@ ENDIF(HDF5_IS_PARALLEL) # Python bindings are built by the standalone scikit-build-core project in # bindings/python/pyalps. The C++ SDK deliberately has no Python dependency. -set(BUILD_BOOST_PYTHON OFF) # Boost Libraries find_package(BoostForALPS REQUIRED) diff --git a/applications/diag/fulldiag/CMakeLists.txt b/applications/diag/fulldiag/CMakeLists.txt index aa48df42e..bd1e3785f 100644 --- a/applications/diag/fulldiag/CMakeLists.txt +++ b/applications/diag/fulldiag/CMakeLists.txt @@ -27,18 +27,7 @@ if(LAPACK_FOUND) add_executable(fulldiag_evaluate fulldiag_evaluate.C) target_link_libraries(fulldiag fulldiag_impl) target_link_libraries(fulldiag_evaluate fulldiag_impl) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(fulldiag PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(fulldiag_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(fulldiag PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(fulldiag_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS fulldiag fulldiag_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS fulldiag fulldiag_evaluate RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "fulldiag will not be built since lapack library has not been found") endif(LAPACK_FOUND) diff --git a/applications/diag/sparsediag/CMakeLists.txt b/applications/diag/sparsediag/CMakeLists.txt index 71f251adc..d3fd8f28a 100644 --- a/applications/diag/sparsediag/CMakeLists.txt +++ b/applications/diag/sparsediag/CMakeLists.txt @@ -22,16 +22,7 @@ if(LAPACK_FOUND) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LAPACK_LINKER_FLAGS}") add_executable(sparsediag sparsediag.C factory.C) target_link_libraries(sparsediag alps ${LAPACK_LIBRARY} ${BLAS_LIBRARY}) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(sparsediag PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(sparsediag PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS sparsediag RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS sparsediag RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "sparsediag will not be built since lapack library is not found") endif(LAPACK_FOUND) diff --git a/applications/dmft/qmc/CMakeLists.txt b/applications/dmft/qmc/CMakeLists.txt index aa95cfe07..394bec0a4 100644 --- a/applications/dmft/qmc/CMakeLists.txt +++ b/applications/dmft/qmc/CMakeLists.txt @@ -115,29 +115,11 @@ if(LAPACK_FOUND) add_executable(dmft_interaction_expansion_choice dmft_interaction_expansion_choice.C) set_property(TARGET dmft_interaction_expansion_choice PROPERTY LABELS dmft) add_alps_test(dmft_interaction_expansion_choice) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(dmft PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(hirschfye PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(hybridization PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(interaction PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dmft PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(hirschfye PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(hybridization PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(interaction PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS dmft RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS hirschfye RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS hybridization RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS interaction RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() install(TARGETS dmft RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS hirschfye RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS hybridization RUNTIME DESTINATION bin COMPONENT applications) install(TARGETS interaction RUNTIME DESTINATION bin COMPONENT applications) install(FILES hybridization/Documentation/hybdoc.pdf DESTINATION doc) -endif() else(LAPACK_FOUND) message(STATUS "dmft will not be built since the lapack library has not been found") endif(LAPACK_FOUND) diff --git a/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex b/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex index 3ac25a4af..701bc2012 100644 --- a/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex +++ b/applications/dmft/qmc/hybridization/Documentation/hybdoc.tex @@ -141,7 +141,7 @@ \subsubsection{Running the solver} \subsection{Python interface} \label{pythoninterface} -If ALPS is built with Python support (parameter \verb#ALPS_BUILD_PYTHON=ON#), the solver is also built as a Python module. It can directly be called from within a Python script. This provides a flexible framework which allows one to easily set up tasks ranging from calculations for multiple parameters to complex selfconsistency schemes. +The standalone pyalps wheel includes the solver module when built with application bindings enabled (the default). It can directly be called from within a Python script. This provides a flexible framework which allows one to easily set up tasks ranging from calculations for multiple parameters to complex selfconsistency schemes. Basic usage of the Python interface is illustrated by the following script, which repeats the previous example for the standalone executable: \begin{verbatim} diff --git a/applications/dmrg/dmrg/CMakeLists.txt b/applications/dmrg/dmrg/CMakeLists.txt index 034689236..18115bcff 100644 --- a/applications/dmrg/dmrg/CMakeLists.txt +++ b/applications/dmrg/dmrg/CMakeLists.txt @@ -26,16 +26,7 @@ else(ALPS_LLVM_WORKAROUND) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LAPACK_LINKER_FLAGS}") add_executable(dmrg dmrg.C factory.C) target_link_libraries(dmrg alps ${LAPACK_LIBRARY} ${BLAS_LIBRARY}) -if(ALPS_PYTHON_WHEEL) - install(TARGETS dmrg RUNTIME DESTINATION pyalps/bin COMPONENT applications) - if(APPLE) - set_target_properties(dmrg PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dmrg PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) -else() install(TARGETS dmrg RUNTIME DESTINATION bin COMPONENT applications) -endif() endif(ALPS_LLVM_WORKAROUND) else(LAPACK_FOUND) message(STATUS "dmrg will not be built since lapack library is not found") diff --git a/applications/mc/simple/CMakeLists.txt b/applications/mc/simple/CMakeLists.txt index 05471ee89..3680f49ae 100644 --- a/applications/mc/simple/CMakeLists.txt +++ b/applications/mc/simple/CMakeLists.txt @@ -19,16 +19,7 @@ add_executable(simplemc main.C evaluator.C ising.C xy.C heisenberg.C) target_link_libraries(simplemc alps) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(simplemc PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(simplemc PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS simplemc RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS simplemc RUNTIME DESTINATION bin COMPONENT applications) -endif() +install(TARGETS simplemc RUNTIME DESTINATION bin COMPONENT applications) enable_testing() add_alps_test(simplemc_ising simplemc ising ising) add_alps_test(simplemc_xy simplemc xy xy) diff --git a/applications/mc/spins/CMakeLists.txt b/applications/mc/spins/CMakeLists.txt index e89ff4364..dcfb6d807 100644 --- a/applications/mc/spins/CMakeLists.txt +++ b/applications/mc/spins/CMakeLists.txt @@ -26,18 +26,7 @@ if(LAPACK_FOUND) add_executable(spinmc_evaluate spinmc_evaluate.C) target_link_libraries(spinmc spinmc_impl) target_link_libraries(spinmc_evaluate spinmc_impl) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(spinmc PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(spinmc_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(spinmc PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(spinmc_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS spinmc spinmc_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS spinmc spinmc_evaluate RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "spins will not be built since lapack library is not found") endif(LAPACK_FOUND) diff --git a/applications/qmc/checksign/CMakeLists.txt b/applications/qmc/checksign/CMakeLists.txt index 90511ce0e..7aef774f1 100644 --- a/applications/qmc/checksign/CMakeLists.txt +++ b/applications/qmc/checksign/CMakeLists.txt @@ -19,13 +19,4 @@ add_executable(checksign checksign.C) target_link_libraries(checksign alps) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(checksign PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(checksign PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS checksign RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS checksign RUNTIME DESTINATION bin COMPONENT applications) -endif() \ No newline at end of file +install(TARGETS checksign RUNTIME DESTINATION bin COMPONENT applications) diff --git a/applications/qmc/looper/CMakeLists.txt b/applications/qmc/looper/CMakeLists.txt index dfdf50c88..1e3ddca04 100644 --- a/applications/qmc/looper/CMakeLists.txt +++ b/applications/qmc/looper/CMakeLists.txt @@ -23,16 +23,7 @@ if(LAPACK_FOUND) include_directories(${PROJECT_SOURCE_DIR}/applications/qmc/looper) add_executable(loop loop.C loop_custom.C loop_model.C path_integral.C sse.C) target_link_libraries(loop alps ${LAPACK_LIBRARY} ${BLAS_LIBRARY}) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(loop PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(loop PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS loop RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS loop RUNTIME DESTINATION bin COMPONENT applications) - endif() else(LAPACK_FOUND) message(STATUS "loop will not be built since lapack library is not found") endif(LAPACK_FOUND) diff --git a/applications/qmc/qwl/CMakeLists.txt b/applications/qmc/qwl/CMakeLists.txt index 6474dd5d4..eb7255733 100644 --- a/applications/qmc/qwl/CMakeLists.txt +++ b/applications/qmc/qwl/CMakeLists.txt @@ -22,15 +22,4 @@ add_executable(qwl_evaluate qwl_evaluate.C) target_link_libraries(qwl alps) target_link_libraries(qwl_evaluate alps) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(qwl PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(qwl_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(qwl PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(qwl_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS qwl qwl_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS qwl qwl_evaluate RUNTIME DESTINATION bin COMPONENT applications) -endif() +install(TARGETS qwl qwl_evaluate RUNTIME DESTINATION bin COMPONENT applications) diff --git a/applications/qmc/sse/CMakeLists.txt b/applications/qmc/sse/CMakeLists.txt index 86e9f2dfc..193621cf3 100644 --- a/applications/qmc/sse/CMakeLists.txt +++ b/applications/qmc/sse/CMakeLists.txt @@ -23,29 +23,11 @@ if(LPSolve_FOUND AND NOT MSVC) SSE.Directed.cpp SSE.Initialization.cpp SSE.Measurements.cpp SSE.Update.cpp SSE.cpp) target_link_libraries(dirloop_sse_v1 alps ${LPSolve_LIBRARIES}) - if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(dirloop_sse_v1 PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dirloop_sse_v1 PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS dirloop_sse_v1 RUNTIME DESTINATION pyalps/bin COMPONENT applications) - else() install(TARGETS dirloop_sse_v1 RUNTIME DESTINATION bin COMPONENT applications) - endif() endif(LPSolve_FOUND AND NOT MSVC) if (LPSolve_FOUND AND APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -idirafter /usr/include/malloc") endif(LPSolve_FOUND AND APPLE) add_executable(dirloop_sse_evaluate evaluate.C) target_link_libraries(dirloop_sse_evaluate alps) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(dirloop_sse_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dirloop_sse_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS dirloop_sse_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS dirloop_sse_evaluate RUNTIME DESTINATION bin COMPONENT applications) -endif() \ No newline at end of file +install(TARGETS dirloop_sse_evaluate RUNTIME DESTINATION bin COMPONENT applications) diff --git a/applications/qmc/sse4/CMakeLists.txt b/applications/qmc/sse4/CMakeLists.txt index 3ad0ef331..996d86f5e 100644 --- a/applications/qmc/sse4/CMakeLists.txt +++ b/applications/qmc/sse4/CMakeLists.txt @@ -19,13 +19,4 @@ add_executable(dirloop_sse main.cc lp_sse.cpp) target_link_libraries(dirloop_sse alps) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(dirloop_sse PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(dirloop_sse PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS dirloop_sse RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS dirloop_sse RUNTIME DESTINATION bin COMPONENT applications) -endif() \ No newline at end of file +install(TARGETS dirloop_sse RUNTIME DESTINATION bin COMPONENT applications) diff --git a/applications/qmc/worms/CMakeLists.txt b/applications/qmc/worms/CMakeLists.txt index 93247388e..823c5fe93 100644 --- a/applications/qmc/worms/CMakeLists.txt +++ b/applications/qmc/worms/CMakeLists.txt @@ -25,17 +25,4 @@ add_executable(worm_evaluate evaluate.C) target_link_libraries(worm worm_impl) target_link_libraries(worm_evaluate worm_impl) -if(ALPS_PYTHON_WHEEL) - if(APPLE) - set_target_properties(worm PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - set_target_properties(worm_evaluate PROPERTIES INSTALL_RPATH "@loader_path/../lib" ) - else(APPLE) - set_target_properties(worm PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - set_target_properties(worm_evaluate PROPERTIES INSTALL_RPATH "$ORIGIN/../lib" ) - endif(APPLE) - install(TARGETS worm RUNTIME DESTINATION pyalps/bin COMPONENT applications) - install(TARGETS worm_evaluate RUNTIME DESTINATION pyalps/bin COMPONENT applications) -else() - install(TARGETS worm RUNTIME DESTINATION bin COMPONENT applications) - install(TARGETS worm_evaluate RUNTIME DESTINATION bin COMPONENT applications) -endif() \ No newline at end of file +install(TARGETS worm worm_evaluate RUNTIME DESTINATION bin COMPONENT applications) diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index e4c3507f3..defebb61a 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -11,6 +11,7 @@ From the repository root: cmake -S . -B _build/alps -G Ninja \ -DCMAKE_INSTALL_PREFIX="$PWD/_build/install" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DALPS_ENABLE_MPI=OFF \ -DALPS_BUILD_LIBS_ONLY=ON cmake --build _build/alps --target install diff --git a/cmake/ALPSConfig.cmake.in b/cmake/ALPSConfig.cmake.in index fdb794cd7..cecdccdc0 100644 --- a/cmake/ALPSConfig.cmake.in +++ b/cmake/ALPSConfig.cmake.in @@ -67,16 +67,6 @@ set(ALPS_BLAS_LIBRARIES "@BLAS_LIBRARIES@") set(ALPS_BLAS_LIBRARY "@BLAS_LIBRARY@") set(ALPS_MKL_INCLUDE_DIR "@MKL_INCLUDE_DIR@") -# Python -set(ALPS_HAVE_PYTHON "@ALPS_HAVE_PYTHON@") -set(ALPS_PYTHON_INTERPRETER "@PYTHON_INTERPRETER@") -set(ALPS_PYTHON_INCLUDE_DIRS "@PYTHON_INCLUDE_DIRS@") -set(ALPS_PYTHON_NUMPY_INCLUDE_DIR "@PYTHON_NUMPY_INCLUDE_DIR@") -set(ALPS_PYTHON_LIBRARY "@PYTHON_LIBRARY@") -set(ALPS_PYTHON_SITE_PKG "@PYTHON_SITE_PKG@") -set(ALPS_PYTHON_EXTRA_LIBS "@PYTHON_EXTRA_LIBS@") -set(ALPS_PYTHON_LINK_FOR_SHARED "@PYTHON_LINK_FOR_SHARED@") - # FFTW set(ALPS_FFTW_LIBRARIES "@FFTW_LIBRARIES@") set(ALPS_FFTW_INCLUDE_DIR "@FFTW_INCLUDE_DIR@") @@ -133,4 +123,3 @@ set(ALPS_EXTRA_LIBRARIES "@ALPS_EXTRA_LIBRARIES@") # list of ALPS and dependent libraries set(ALPS_LIBRARIES alps ${ALPS_Boost_LIBRARIES} ${ALPS_EXTRA_LIBRARIES} CACHE STRING "List of ALPS and dependent libraries." FORCE) set(ALPS_FORTRAN_LIBRARIES alps_fortran CACHE STRING "List of ALPS-Fortran library." FORCE) - diff --git a/cmake/FindBoostForALPS.cmake b/cmake/FindBoostForALPS.cmake index 7038bb766..8fbed54b7 100644 --- a/cmake/FindBoostForALPS.cmake +++ b/cmake/FindBoostForALPS.cmake @@ -67,91 +67,6 @@ if(ALPS_USE_SYSTEM_BOOST) "Upgrade the system Boost installation or disable ALPS_USE_SYSTEM_BOOST.") endif() - # Save Boost_LIBRARIES now — a second find_package(Boost) call below - # (for the Python component) would overwrite this variable. - set(_alps_boost_libraries_saved ${Boost_LIBRARIES}) - - # Python component: library naming varies by Boost/distro version. - # Try python (e.g. python311), then python3, then python. - if(ALPS_HAVE_PYTHON) - set(_alps_python_component "") - set(_alps_python_library "") - foreach(_pycomp "python${PYVER}" "python3" "python") - find_package(Boost QUIET COMPONENTS ${_pycomp}) - if(Boost_${_pycomp}_FOUND) - # Capture Boost_LIBRARIES right here: after a single-component - # find_package it contains exactly that one library path. - set(_alps_python_library ${Boost_LIBRARIES}) - set(_alps_python_component ${_pycomp}) - break() - endif() - endforeach() - - # Restore the full library list (overwritten by the python find_package). - if(_alps_python_component) - message(STATUS "Found system Boost.Python component: ${_alps_python_component}") - set(Boost_LIBRARIES ${_alps_boost_libraries_saved} ${_alps_python_library}) - else() - message(WARNING - "System Boost.Python library not found (tried python${PYVER}, python3, python). " - "Python bindings will be disabled.") - set(Boost_LIBRARIES ${_alps_boost_libraries_saved}) - set(ALPS_HAVE_PYTHON OFF) - set(BUILD_BOOST_PYTHON OFF) - endif() - endif() - - # Set ALPS_HAVE_BOOST_NUMPY for Boost >= 1.63 (when boost::python::numpy - # was introduced). - if(Boost_VERSION_STRING VERSION_GREATER_EQUAL "1.63.0") - set(ALPS_HAVE_BOOST_NUMPY ON) - endif() - - # Scenario 3: system Boost 1.63-1.86 + NumPy >= 2.0. - # (evaluated below; set the flag early so the numpy lib search is guarded by it) - # boost::python::numpy in these versions uses deprecated NumPy C API - # removed in NumPy 2.0. Fall back to boost::python::numeric::array, - # which uses only the stable NumPy C API. - if(ALPS_HAVE_BOOST_NUMPY AND ALPS_HAVE_PYTHON) - EXEC_PYTHON_SCRIPT("import numpy; print(numpy.__version__)" _alps_numpy_ver) - message(STATUS "NumPy version: ${_alps_numpy_ver}") - if(_alps_numpy_ver VERSION_GREATER_EQUAL "2.0.0" AND - Boost_VERSION_STRING VERSION_LESS "1.87.0") - message(WARNING - "System Boost ${Boost_VERSION_STRING} does not support NumPy >= 2.0 " - "(requires Boost >= 1.87). " - "Falling back to boost::python::numeric::array. " - "Upgrade system Boost to >= 1.87 to silence this warning.") - set(ALPS_HAVE_BOOST_NUMPY OFF) - endif() - endif() - - # Boost.NumPy library: only link when ALPS_HAVE_BOOST_NUMPY is still ON - # after the scenario-3 check above. Library naming mirrors python: try - # numpy, numpy3, numpy. - if(ALPS_HAVE_BOOST_NUMPY AND ALPS_HAVE_PYTHON) - set(_alps_boost_libs_before_numpy ${Boost_LIBRARIES}) - set(_alps_numpy_lib_found "") - foreach(_npcomp "numpy${PYVER}" "numpy3" "numpy") - find_package(Boost QUIET COMPONENTS ${_npcomp}) - if(Boost_${_npcomp}_FOUND) - message(STATUS "Found system Boost.NumPy component: ${_npcomp}") - # Boost_LIBRARIES is now just the numpy lib — capture and restore. - set(_alps_numpy_library ${Boost_LIBRARIES}) - set(Boost_LIBRARIES ${_alps_boost_libs_before_numpy} ${_alps_numpy_library}) - set(_alps_numpy_lib_found TRUE) - break() - endif() - endforeach() - if(NOT _alps_numpy_lib_found) - message(WARNING - "System Boost.NumPy library not found (tried numpy${PYVER}, numpy3, numpy). " - "Falling back to boost::python::numeric::array.") - set(Boost_LIBRARIES ${_alps_boost_libs_before_numpy}) - set(ALPS_HAVE_BOOST_NUMPY OFF) - endif() - endif() - # Align Boost_INCLUDE_DIR (singular) used elsewhere in the build. set(Boost_INCLUDE_DIR ${Boost_INCLUDE_DIRS}) @@ -165,8 +80,6 @@ if(ALPS_USE_SYSTEM_BOOST) message(STATUS "Using system Boost ${Boost_VERSION_STRING}") message(STATUS " includes: ${Boost_INCLUDE_DIRS}") message(STATUS " libraries: ${Boost_LIBRARIES}") - message(STATUS " ALPS_HAVE_BOOST_NUMPY: ${ALPS_HAVE_BOOST_NUMPY}") - return() endif() # ALPS_USE_SYSTEM_BOOST diff --git a/cmake/FindBoostSrc.cmake b/cmake/FindBoostSrc.cmake index a9029a24b..76977602a 100644 --- a/cmake/FindBoostSrc.cmake +++ b/cmake/FindBoostSrc.cmake @@ -40,10 +40,6 @@ if (NOT DEFINED BUILD_BOOST_SYSTEM) set(BUILD_BOOST_SYSTEM TRUE) endif (NOT DEFINED BUILD_BOOST_SYSTEM) -if (NOT DEFINED BUILD_BOOST_PYTHON) - set(BUILD_BOOST_PYTHON TRUE) -endif(NOT DEFINED BUILD_BOOST_PYTHON) - if (NOT DEFINED BUILD_BOOST_THREAD) set(BUILD_BOOST_THREAD TRUE) endif (NOT DEFINED BUILD_BOOST_THREAD) @@ -95,11 +91,6 @@ if(Boost_INCLUDE_DIR) MATH(EXPR Boost_SUBMINOR_VERSION "${Boost_VERSION} % 100") endif(Boost_INCLUDE_DIR) -if(Boost_VERSION AND NOT Boost_VERSION LESS 106300) - # Boost Numpy is compiled if we have >= 1.63 - set(ALPS_HAVE_BOOST_NUMPY ON) -endif(Boost_VERSION AND NOT Boost_VERSION LESS 106300) - if(Boost_ROOT_DIR) message(STATUS "Found Boost Source: ${Boost_ROOT_DIR}") message(STATUS "Boost Version: ${Boost_MAJOR_VERSION}_${Boost_MINOR_VERSION}_${Boost_SUBMINOR_VERSION}") @@ -110,19 +101,6 @@ else(Boost_ROOT_DIR) message(FATAL_ERROR "Boost Source not Found") endif(Boost_ROOT_DIR) -if(BUILD_BOOST_PYTHON) - EXEC_PYTHON_SCRIPT ("import numpy; print(numpy.__version__)" numpy_ver) - MESSAGE(STATUS "numpy version ${numpy_ver}" ) - if(${numpy_ver} VERSION_GREATER_EQUAL "2.0.0" AND "${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}" VERSION_LESS "1.87.0" ) - message(WARNING - "Boost ${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION} " - "does not support NumPy >= 2.0 (requires Boost >= 1.87). " - "Falling back to boost::python::numeric::array. " - "Upgrade to Boost >= 1.87 to silence this warning.") - set(ALPS_HAVE_BOOST_NUMPY OFF) - endif() -endif() - # Avoid auto link of Boost library add_definitions(-DBOOST_ALL_NO_LIB=1) if(BUILD_SHARED_LIBS) diff --git a/cmake/FindPythonMod.cmake b/cmake/FindPythonMod.cmake deleted file mode 100644 index cec93bf8a..000000000 --- a/cmake/FindPythonMod.cmake +++ /dev/null @@ -1,275 +0,0 @@ -# Copyright Olivier Parcollet and Matthias Troyer 2010. -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the “Software”), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -# -# Python settings : -# -# This module checks that : -# - the python interpreter is working and version >= 2.6 -# - it has modules : distutils, numpy, tables, scipy -# -# This module defines the variables -# - PYTHON_INTERPRETER : name of the python interpreter -# - PYTHON_INCLUDE_DIRS : include for compilation -# - PYTHON_NUMPY_INCLUDE_DIR : include for compilation with numpy -# - PYTHON_LIBRARY : link flags -# - PYTHON_SITE_PKG : path to the standard packages of the python interpreter -# - PYTHON_EXTRA_LIBS : libraries which must be linked in when embedding -# - PYTHON_LINK_FOR_SHARED : linking flags needed when building a shared lib for external modules - -message(STATUS "Search for Python") - -if (NOT PYTHON_INTERPRETER) - #find_program(PYTHON_INTERPRETER NAMES python3 python PATHS $ENV{PATH}) - if(ALPS_PYTHON_WHEEL OR ALPS_BUILD_LIBS_ONLY) - find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) - set(PYTHON_LIBRARY Python::Module) - else(ALPS_PYTHON_WHEEL OR ALPS_BUILD_LIBS_ONLY) - find_package(Python COMPONENTS Interpreter Development REQUIRED) - set(PYTHON_LIBRARY Python::Python) - endif(ALPS_PYTHON_WHEEL OR ALPS_BUILD_LIBS_ONLY) - set(PYTHON_INTERPRETER ${Python_EXECUTABLE}) -message(STATUS "LIBS: ${Python_LIBRARY} ${Python_LIBRARIES}") - if (NOT PYTHON_INTERPRETER) - set (PYTHON_FOUND FALSE) - else(NOT PYTHON_INTERPRETER) - set(PYTHON_FOUND TRUE) - endif(NOT PYTHON_INTERPRETER) -else (NOT PYTHON_INTERPRETER) - set(PYTHON_FOUND TRUE) -endif (NOT PYTHON_INTERPRETER) - -set(PYTHON_MINIMAL_VERSION 3.9) - -if (WIN32) - MESSAGE (STATUS "Looking for PythonLibs") - find_package(PythonLibs) -endif (WIN32) - -IF (PYTHON_FOUND) - - MESSAGE (STATUS "Python interpreter ${PYTHON_INTERPRETER}") - # - # The function EXEC_PYTHON_SCRIPT executes the_script in python interpreter - # and set the variable of output_var_name in the calling scope - # - FUNCTION ( EXEC_PYTHON_SCRIPT the_script output_var_name) - EXECUTE_PROCESS(COMMAND ${PYTHON_INTERPRETER} -c "${the_script}" - OUTPUT_VARIABLE res RESULT_VARIABLE returncode OUTPUT_STRIP_TRAILING_WHITESPACE) - IF (NOT returncode EQUAL 0) - MESSAGE(FATAL_ERROR "The script : ${the_script} \n did not run properly in the Python interpreter. Check your python installation.") - ENDIF (NOT returncode EQUAL 0) - SET( ${output_var_name} ${res} PARENT_SCOPE) - ENDFUNCTION (EXEC_PYTHON_SCRIPT) - - # - # Check the interpreter and its version - # - EXEC_PYTHON_SCRIPT ("import sys, string; print(sys.version.split()[0])" PYTHON_VERSION) -# STRING(COMPARE GREATER ${PYTHON_MINIMAL_VERSION} ${PYTHON_VERSION} PYTHON_VERSION_NOT_OK) -# IF (PYTHON_VERSION_NOT_OK) - IF( ${PYTHON_VERSION} VERSION_LESS ${PYTHON_MINIMAL_VERSION} ) - MESSAGE(WARNING "Python intepreter version is ${PYTHON_VERSION} . It should be >= ${PYTHON_MINIMAL_VERSION}") - SET(PYTHON_FOUND FALSE) - ENDIF () - EXEC_PYTHON_SCRIPT("import sys; print('{}{}'.format(sys.version_info.major,sys.version_info.minor))" PYVER) # e.g. 27, 38 -ENDIF (PYTHON_FOUND) - -IF (PYTHON_FOUND) - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("import distutils " nulle) # check that distutils is there... - else() - EXEC_PYTHON_SCRIPT ("import sysconfig " nulle) # check that distutils is there... - endif() - EXEC_PYTHON_SCRIPT ("import numpy" nulle) # check that numpy is there... - #EXEC_PYTHON_SCRIPT ("import scipy" nulle) # check that scipy is there... - #EXEC_PYTHON_SCRIPT ("import tables" nulle) # check that tables is there... - MESSAGE(STATUS "Python interpreter ok : version ${PYTHON_VERSION}" ) - - # - # Python function to normalize linker flags - # - # Goal: CMake has two requiriments on the library flags: - # 1. the string cannot start with a spaces - # 2. if the string starts with a slash, the argument is interpreted as *a single library name* or a list of libraries - # this is broken if the linker flags are, e.g. "/path/to/lib -framework MyFramework -sysroot /" - # --> we need to split the string into a list of elements starting with "/" or "-". - # TODO: there might be problems if some path contains spaces - set(PYFUNC_NORMALIZE_FLAGS "def normalize_flags(flags):\n flags=flags.strip()\n if flags[0]=='-':return flags\n parts=flags.split(' ', 1)\n if len(parts)>0:return parts[0].strip()+';'+normalize_flags(parts[1])\n return parts[0].strip()\n") - - # - # Check for Python include path - # - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("import distutils ; from distutils.sysconfig import * ; print(distutils.sysconfig.get_python_inc())" PYTHON_INCLUDE_DIRS ) - else() - EXEC_PYTHON_SCRIPT ("import sysconfig ; print(sysconfig.get_path('include'))" PYTHON_INCLUDE_DIRS ) - endif() - message(STATUS "PYTHON_INCLUDE_DIRS = ${PYTHON_INCLUDE_DIRS}" ) - mark_as_advanced(PYTHON_INCLUDE_DIRS) - FIND_PATH(TEST_PYTHON_INCLUDE patchlevel.h PATHS ${PYTHON_INCLUDE_DIRS} NO_DEFAULT_PATH) - if (NOT TEST_PYTHON_INCLUDE) - message (ERROR "The Python header files have not been found. Please check that you installed the Python headers and not only the interpreter.") - endif (NOT TEST_PYTHON_INCLUDE) - - # - # include files for numpy - # - EXEC_PYTHON_SCRIPT ("import numpy;print(numpy.get_include())" PYTHON_NUMPY_INCLUDE_DIR) - MESSAGE(STATUS "PYTHON_NUMPY_INCLUDE_DIR = ${PYTHON_NUMPY_INCLUDE_DIR}" ) - mark_as_advanced(PYTHON_NUMPY_INCLUDE_DIR) - - # - # Check for site packages - # - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("from distutils.sysconfig import * ;print(get_python_lib(0,0))" - PYTHON_SITE_PKG) - else() - EXEC_PYTHON_SCRIPT ("import sysconfig ; print(sysconfig.get_path('purelib'))" - PYTHON_SITE_PKG) - endif() - MESSAGE(STATUS "PYTHON_SITE_PKG = ${PYTHON_SITE_PKG}" ) - mark_as_advanced(PYTHON_SITE_PKG) - if (NOT WIN32) - if(NOT PYTHON_LIBRARY) - # - # Check for Python library path - # - #EXEC_PYTHON_SCRIPT ("import string; from distutils.sysconfig import * ;print string.join(get_config_vars('VERSION'))" PYTHON_VERSION_MAJOR_MINOR) - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("import string; from distutils.sysconfig import *; print(' '.join(get_config_vars('LIBDIR')))" PYTHON_LIBRARY_BASE_PATH) - # this is the static libpython which is not always correct. it is better to give precedence to the shared one. - # EXEC_PYTHON_SCRIPT ("from distutils.sysconfig import *; print(get_config_vars('LIBRARY')[0])" PYTHON_LIBRARY_BASE_FILE) - EXEC_PYTHON_SCRIPT ("from distutils.sysconfig import *; print('libpython{}'.format(' '.join(get_config_vars('VERSION'))))" PYTHON_LIBRARY_BASE_FILE) - else() - - EXEC_PYTHON_SCRIPT ("import string; from sysconfig import *; print(' '.join(get_config_vars('LIBDIR')))" PYTHON_LIBRARY_BASE_PATH) - # this is the static libpython which is not always correct. it is better to give precedence to the shared one. - # EXEC_PYTHON_SCRIPT ("from distutils.sysconfig import *; print(get_config_vars('LIBRARY')[0])" PYTHON_LIBRARY_BASE_FILE) - EXEC_PYTHON_SCRIPT ("from sysconfig import *; print('libpython{}'.format(' '.join(get_config_vars('VERSION'))))" PYTHON_LIBRARY_BASE_FILE) - endif() - IF(BUILD_SHARED_LIBS) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}.so" PATHS ${PYTHON_LIBRARY_BASE_PATH}) - IF(NOT PYTHON_LIBRARY) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}m.so" PATHS ${PYTHON_LIBRARY_BASE_PATH}) - ENDIF(NOT PYTHON_LIBRARY) - IF(NOT PYTHON_LIBRARY) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}.a" PATHS ${PYTHON_LIBRARY_BASE_PATH}) - ENDIF(NOT PYTHON_LIBRARY) - IF(NOT PYTHON_LIBRARY) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}m.a" PATHS ${PYTHON_LIBRARY_BASE_PATH}) - ENDIF(NOT PYTHON_LIBRARY) - ELSE(BUILD_SHARED_LIBS) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}.a" PATHS ${PYTHON_LIBRARY_BASE_PATH}) - ENDIF(BUILD_SHARED_LIBS) - IF(NOT PYTHON_LIBRARY) - # On Debian/Ubuntu system, libpython*.so is located in /usr/lib/`gcc -print-multiarch` - execute_process(COMMAND gcc -print-multiarch OUTPUT_VARIABLE TRIPLES) - STRING(REGEX REPLACE "\n" "" TRIPLES ${TRIPLES}) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}.so" PATHS "/usr/lib/${TRIPLES}") - IF(NOT PYTHON_LIBRARY) - FIND_FILE(PYTHON_LIBRARY NAMES "${PYTHON_LIBRARY_BASE_FILE}.a" PATHS "/usr/lib/${TRIPLES}") - ENDIF(NOT PYTHON_LIBRARY) - ENDIF(NOT PYTHON_LIBRARY) - endif(NOT PYTHON_LIBRARY) - MESSAGE(STATUS "PYTHON_LIBRARY = ${PYTHON_LIBRARY}" ) - mark_as_advanced(PYTHON_LIBRARY) - - # - # libraries which must be linked in when embedding - # - if(NOT DEFINED PYTHON_EXTRA_LIBS) - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("${PYFUNC_NORMALIZE_FLAGS}from distutils.sysconfig import * ;print( normalize_flags( str(get_config_var('LOCALMODLIBS')) + ' ' + str(get_config_var('LIBS')) + ' ' + str(get_config_var('LDFLAGS')) ))" - PYTHON_EXTRA_LIBS) - else() - EXEC_PYTHON_SCRIPT ("${PYFUNC_NORMALIZE_FLAGS}from sysconfig import * ;print( normalize_flags( str(get_config_var('LOCALMODLIBS')) + ' ' + str(get_config_var('LIBS')) + ' ' + str(get_config_var('LDFLAGS')) ))" - PYTHON_EXTRA_LIBS) - endif() - endif() - MESSAGE(STATUS "PYTHON_EXTRA_LIBS =${PYTHON_EXTRA_LIBS}" ) - mark_as_advanced(PYTHON_EXTRA_LIBS) - - # - # linking flags needed when embedding (building a shared lib) - # To BE RETESTED - # - if(PYTHON_VERSION VERSION_LESS "3.11") - EXEC_PYTHON_SCRIPT ("from distutils.sysconfig import *;print(get_config_var('LINKFORSHARED'))" - PYTHON_LINK_FOR_SHARED) - else() - EXEC_PYTHON_SCRIPT ("from sysconfig import *;print(get_config_var('LINKFORSHARED'))" - PYTHON_LINK_FOR_SHARED) - endif() - MESSAGE(STATUS "PYTHON_LINK_FOR_SHARED = ${PYTHON_LINK_FOR_SHARED}" ) - mark_as_advanced(PYTHON_LINK_FOR_SHARED) - endif(NOT WIN32) - - # Correction on Mac - IF(APPLE) - SET (PYTHON_LINK_FOR_SHARED -u _PyMac_Error -framework Python) - SET (PYTHON_LINK_MODULE -bundle -undefined dynamic_lookup) - ELSE(APPLE) - SET (PYTHON_LINK_MODULE -shared) - ENDIF(APPLE) -ENDIF (PYTHON_FOUND) - -set (PYTHONLIBS_FOUND ${PYTHON_FOUND}) - - -EXEC_PYTHON_SCRIPT("import sys; print('{}.{}'.format(sys.version_info.major,sys.version_info.minor))" PYVER) # e.g. 27, 38 -set(ALPS_PYTHON_LIB_DEST_ROOT lib/python${PYVER}/site-packages CACHE PATH "Module install path") - -# -# This function writes down a script to compile f2py modules -# indeed, one needs to use the f2py of the correct numpy module. -# -FUNCTION( WriteScriptToBuildF2pyModule filename fcompiler_desc modulename module_pyf_name filelist ) - # Copy all the files - EXECUTE_PROCESS(COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/${module_pyf_name} ${CMAKE_CURRENT_BINARY_DIR} ) - FOREACH( f ${filelist}) - EXECUTE_PROCESS(COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/${f} ${CMAKE_CURRENT_BINARY_DIR} ) - ENDFOREACH(f) - # write the script that will build the f2py extension - SET(filename ${CMAKE_CURRENT_BINARY_DIR}/${filename} ) - FILE(WRITE ${filename} "import sys\n") - FILE(APPEND ${filename} "from numpy.f2py import main\n") - FILE(APPEND ${filename} "sys.argv = [''] +'-c --fcompiler=${fcompiler_desc} -m ${modulename} ${modulename}.pyf ${filelist} -llapack'.split()\n") - FILE(APPEND ${filename} "main()\n") -ENDFUNCTION(WriteScriptToBuildF2pyModule) - -FUNCTION(PYTHON_ADD_MODULE _NAME ) - OPTION(PYTHON_ENABLE_MODULE_${_NAME} "Add module ${_NAME}" TRUE) - OPTION(PYTHON_MODULE_${_NAME}_BUILD_SHARED "Add module ${_NAME} shared" ${BUILD_SHARED_LIBS}) - - IF(PYTHON_ENABLE_MODULE_${_NAME}) - IF(PYTHON_MODULE_${_NAME}_BUILD_SHARED) - SET(PY_MODULE_TYPE MODULE) - ELSE(PYTHON_MODULE_${_NAME}_BUILD_SHARED) - SET(PY_MODULE_TYPE STATIC) - SET_PROPERTY(GLOBAL APPEND PROPERTY PY_STATIC_MODULES_LIST ${_NAME}) - ENDIF(PYTHON_MODULE_${_NAME}_BUILD_SHARED) - - SET_PROPERTY(GLOBAL APPEND PROPERTY PY_MODULES_LIST ${_NAME}) - ADD_LIBRARY(${_NAME} ${PY_MODULE_TYPE} ${ARGN}) -# TARGET_LINK_LIBRARIES(${_NAME} ${PYTHON_LIBRARIES}) - - ENDIF(PYTHON_ENABLE_MODULE_${_NAME}) -ENDFUNCTION(PYTHON_ADD_MODULE) diff --git a/cmake/UseALPS.cmake b/cmake/UseALPS.cmake index 0b4160df0..1e9c5b356 100644 --- a/cmake/UseALPS.cmake +++ b/cmake/UseALPS.cmake @@ -57,15 +57,6 @@ if(NOT ALPS_USE_FILE_INCLUDED) set(BLAS_LIBRARY ${ALPS_BLAS_LIBRARY}) set(MKL_INCLUDE_DIR ${ALPS_MKL_INCLUDE_DIR}) - # Python - set(PYTHON_INTERPRETER ${ALPS_PYTHON_INTERPRETER}) - set(PYTHON_INCLUDE_DIRS ${ALPS_PYTHON_INCLUDE_DIRS}) - set(PYTHON_NUMPY_INCLUDE_DIR ${ALPS_PYTHON_NUMPY_INCLUDE_DIR}) - set(PYTHON_LIBRARY ${ALPS_PYTHON_LIBRARY}) - set(PYTHON_SITE_PKG ${ALPS_PYTHON_SITE_PKG}) - set(PYTHON_EXTRA_LIBS ${ALPS_PYTHON_EXTRA_LIBS}) - set(PYTHON_LINK_FOR_SHARED ${ALPS_PYTHON_LINK_FOR_SHARED}) - # FFTW set(FFTW_LIBRARIES ${ALPS_FFTW_LIBRARIES}) set(FFTW_INCLUDE_DIR ${ALPS_FFTW_INCLUDE_DIR}) diff --git a/src/alps/config.h.in b/src/alps/config.h.in index 394e18a4b..d69404bbf 100644 --- a/src/alps/config.h.in +++ b/src/alps/config.h.in @@ -121,16 +121,6 @@ // Define to 1 if you use Xerces C++ XML parser by Apache Software Foundation. #cmakedefine ALPS_HAVE_XERCES_PARSER -// -// Python -// - -// Define to 1 if you have Python on your system. -//#cmakedefine ALPS_HAVE_PYTHON - -// Define to 1 if Boost Numpy (>=1.63) is available -//#cmakedefine ALPS_HAVE_BOOST_NUMPY - // // OpenMP // diff --git a/src/boost/CMakeLists.txt b/src/boost/CMakeLists.txt index 30274bb1a..fb8fb09af 100644 --- a/src/boost/CMakeLists.txt +++ b/src/boost/CMakeLists.txt @@ -22,10 +22,7 @@ # set(BOOST_SOURCES "") -set(BOOST_PYTHON_SOURCES "") -set(BOOST_MPI_PYTHON_SOURCES "") set(BOOST_LINK_LIBS "") -set(BOOST_PYTHON_LINK_LIBS "") # Boost.Date_Time if(BUILD_BOOST_DATE_TIME) @@ -169,40 +166,6 @@ if(BUILD_BOOST_SERIALIZATION) add_definitions(-DBOOST_SERIALIZATION_DYN_LINK=1) endif(BUILD_BOOST_SERIALIZATION) -# Boost.Python -if(BUILD_BOOST_PYTHON) - if(PYTHON_VERSION GREATER 3 AND Boost_MAJOR_VERSION EQUAL 1 AND Boost_MINOR_VERSION LESS 63) - message(WARNING "Python 3 support requires Boost 1.63.0 or newer. Previous versions might build but have sporadic segemtation faults at the end of the execution.") - endif() - if(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - set(BOOST_PYTHON_LINK_LIBS ${BOOST_LINK_LIBS} ${PYTHON_LIBRARY}) # ${PYTHON_EXTRA_LIBS}) - else(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - set(BOOST_PYTHON_LINK_LIBS ${BOOST_LINK_LIBS} ${PYTHON_LIBRARY} ${PYTHON_EXTRA_LIBS}) - endif(ALPS_PYTHON_WHEEL AND PYTHON_VERSION VERSION_GREATER_EQUAL "3.13" AND APPLE) - set(DIRECTORY "${Boost_ROOT_DIR}/libs/python/src") - set(SOURCES dict.cpp errors.cpp exec.cpp import.cpp list.cpp long.cpp - module.cpp numeric.cpp object_operators.cpp object_protocol.cpp slice.cpp - str.cpp tuple.cpp wrapper.cpp converter/arg_to_python_base.cpp - converter/builtin_converters.cpp converter/from_python.cpp - converter/registry.cpp converter/type_id.cpp object/class.cpp - object/enum.cpp object/function.cpp object/function_doc_signature.cpp - object/inheritance.cpp object/iterator.cpp object/life_support.cpp - object/pickle_support.cpp object/stl_iterator.cpp - ) - if(Boost_MAJOR_VERSION EQUAL 1 AND Boost_MINOR_VERSION GREATER 62 AND ALPS_HAVE_BOOST_NUMPY) - set(SOURCES ${SOURCES} - numpy/dtype.cpp numpy/matrix.cpp numpy/ndarray.cpp - numpy/numpy.cpp numpy/scalars.cpp numpy/ufunc.cpp - ) - endif(Boost_MAJOR_VERSION EQUAL 1 AND Boost_MINOR_VERSION GREATER 62 AND ALPS_HAVE_BOOST_NUMPY) - foreach(S ${SOURCES}) - if(EXISTS ${DIRECTORY}/${S}) - set(BOOST_PYTHON_SOURCES ${BOOST_PYTHON_SOURCES} ${DIRECTORY}/${S}) - endif(EXISTS ${DIRECTORY}/${S}) - endforeach(S) - add_definitions(-DBOOST_PYTHON_SOURCE) -endif(BUILD_BOOST_PYTHON) - # Boost.System if(BUILD_BOOST_SYSTEM) set(DIRECTORY "${Boost_ROOT_DIR}/libs/system/src") @@ -261,43 +224,6 @@ if(BUILD_BOOST_THREAD) endif(BUILD_BOOST_THREAD) -# Boost.MPI Python bindings -if (BUILD_BOOST_MPI AND BUILD_BOOST_PYTHON AND Boost_ROOT_DIR) - set(DIRECTORY "${Boost_ROOT_DIR}/libs/mpi/src/python") - set(SOURCES collectives.cpp py_communicator.cpp datatypes.cpp - documentation.cpp py_environment.cpp py_nonblocking.cpp py_exception.cpp - py_request.cpp skeleton_and_content.cpp status.cpp py_timer.cpp serialize.cpp - ) - foreach(S ${SOURCES}) - if(EXISTS ${DIRECTORY}/${S}) - set(BOOST_MPI_PYTHON_SOURCES ${BOOST_MPI_PYTHON_SOURCES} ${DIRECTORY}/${S}) - endif(EXISTS ${DIRECTORY}/${S}) - endforeach(S) - - # renmae mpi module to mpi_c - set(BOOST_MPI_PYTHON_SOURCES ${BOOST_MPI_PYTHON_SOURCES} mpi/module.cpp) - - if (BOOST_MPI_PYTHON_SOURCES) - python_add_module(mpi_c ${BOOST_MPI_PYTHON_SOURCES}) - if(BUILD_SHARED_LIBS) - set_target_properties(mpi_c PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - if(WIN32 AND NOT UNIX) - set_target_properties(mpi_c PROPERTIES SUFFIX ".pyd") - endif(WIN32 AND NOT UNIX) - endif (BUILD_SHARED_LIBS) - - set_target_properties(mpi_c PROPERTIES PREFIX "") - target_link_libraries(mpi_c ${ALPS_BOOST_LIBRARY_NAME} ${ALPS_BOOST_PYTHON_LIBRARY_NAME} ${BOOST_LINK_LIBS} ${BOOST_PYTHON_LINK_LIBS}) - - install(TARGETS mpi_c COMPONENT python - RUNTIME DESTINATION bin - ARCHIVE DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps - LIBRARY DESTINATION ${ALPS_PYTHON_LIB_DEST_ROOT}/pyalps) - endif(BOOST_MPI_PYTHON_SOURCES) - -endif (BUILD_BOOST_MPI AND BUILD_BOOST_PYTHON AND Boost_ROOT_DIR) - - ####################################################################### # install ####################################################################### @@ -307,10 +233,6 @@ endif (BUILD_BOOST_MPI AND BUILD_BOOST_PYTHON AND Boost_ROOT_DIR) if (NOT Boost_FOUND) add_library(${ALPS_BOOST_LIBRARY_NAME} ${BOOST_SOURCES}) target_link_libraries(${ALPS_BOOST_LIBRARY_NAME} ${BOOST_LINK_LIBS}) - if(BUILD_BOOST_PYTHON) - add_library(${ALPS_BOOST_PYTHON_LIBRARY_NAME} ${BOOST_PYTHON_SOURCES}) - target_link_libraries(${ALPS_BOOST_PYTHON_LIBRARY_NAME} ${BOOST_PYTHON_LINK_LIBS}) - endif() # Boost.Test if(BUILD_BOOST_TEST) set(DIRECTORY "${Boost_ROOT_DIR}/libs/test/src") @@ -390,32 +312,15 @@ if (NOT Boost_FOUND) COMMAND ${CMAKE_COMMAND} -E copy ${LIB_NAME} ${PROJECT_BINARY_DIR}/bin) endif(MSVC) - if(NOT ALPS_PYTHON_WHEEL) install(TARGETS ${ALPS_BOOST_LIBRARY_NAME} COMPONENT libraries RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib) - if(BUILD_BOOST_PYTHON) - install(TARGETS ${ALPS_BOOST_PYTHON_LIBRARY_NAME} COMPONENT libraries - RUNTIME DESTINATION bin - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib) - endif() - if (ALPS_INSTALL_BOOST_TEST) - install(TARGETS boost_unit_test_framework boost_test_exec_monitor boost_prg_exec_monitor + if (ALPS_INSTALL_BOOST_TEST) + install(TARGETS boost_unit_test_framework boost_test_exec_monitor boost_prg_exec_monitor COMPONENT libraries RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib) - endif(ALPS_INSTALL_BOOST_TEST) - else () - install(TARGETS ${ALPS_BOOST_LIBRARY_NAME} COMPONENT libraries - RUNTIME DESTINATION pyalps/bin - ARCHIVE DESTINATION pyalps/lib - LIBRARY DESTINATION pyalps/lib) - install(TARGETS ${ALPS_BOOST_PYTHON_LIBRARY_NAME} COMPONENT libraries - RUNTIME DESTINATION pyalps/bin - ARCHIVE DESTINATION pyalps/lib - LIBRARY DESTINATION pyalps/lib) - endif() + endif(ALPS_INSTALL_BOOST_TEST) endif (NOT Boost_FOUND) diff --git a/test/pyalps/CMakeLists.txt b/test/pyalps/CMakeLists.txt deleted file mode 100644 index f1c5271e3..000000000 --- a/test/pyalps/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright Matthias Troyer, Synge Todo and Lukas Gamper 2009 - 2010. -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the “Software”), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -include_directories(${PROJECT_BINARY_DIR}/src) -include_directories(${PROJECT_SOURCE_DIR}/src) -include_directories(${Boost_ROOT_DIR}) - -#add_executable(loadobs loadobs.cpp) -#add_dependencies(loadobs alps) -#target_link_libraries(loadobs alps) -#add_alps_test(loadobs) - -enable_testing() -if (ALPS_BUILD_PYTHON AND BUILD_SHARED_LIBS) -# FOREACH (name pyioarchive pyhdf5io numpylarge pyparams hlist_test mcdata pyhdf5 mcanalyze, accumulators) - FOREACH (name pyioarchive pyhdf5io_test pyparams_test hlist_test mcdata_test pyhdf5_test mcanalyze) - add_test(python_${name} - ${CMAKE_COMMAND} - -Dpython_interpreter=${PYTHON_INTERPRETER} - -Dcmd=${name}.py - -Dinput=${name} - -Doutput=${name} - -Dpythonpath=${PROJECT_BINARY_DIR}/lib/pyalps:${PROJECT_SOURCE_DIR}/lib - -Dsourcedir=${CMAKE_CURRENT_SOURCE_DIR} - -Dbinarydir=${CMAKE_CURRENT_BINARY_DIR} - -Dcmddir=${CMAKE_CURRENT_SOURCE_DIR} - -P ${CMAKE_CURRENT_SOURCE_DIR}/run_python_test.cmake - ) - set_property(TEST python_${name} PROPERTY LABELS pyalps) - ENDFOREACH(name) -ENDIF(ALPS_BUILD_PYTHON AND BUILD_SHARED_LIBS) diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt index 51070a2d7..63853c01d 100644 --- a/tool/CMakeLists.txt +++ b/tool/CMakeLists.txt @@ -93,30 +93,3 @@ endif(SQLite_FOUND) target_link_libraries(maxent_linear_grid_numeric alps ${LAPACK_LIBRARY} ${BLAS_LIBRARY}) add_alps_test(maxent_linear_grid_numeric) endif(LAPACK_FOUND) - - # - # alpspython script - # - - set(ALPSPYTHON_CONFIGURED FALSE) - if(PYTHON_INTERPRETER) - if (NOT WIN32) - set(PYTHONPATH "${ALPS_PYTHON_LIB_DEST_ROOT}") - set(PYTHONBIN "${PYTHON_INTERPRETER}") - string(CONFIGURE [[ - set(PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@") - set(PYTHONBIN "@PYTHONBIN@") - set(PYTHONPATH "${CMAKE_INSTALL_PREFIX}/@PYTHONPATH@") - message(STATUS ": ${PYTHONPATH}") - message(STATUS ": ${PYTHONBIN}") - configure_file(${PROJECT_SOURCE_DIR}/tool/alpspython.in ${CMAKE_INSTALL_PREFIX}/bin/alpspython ) - ]] install_script @ONLY) - install(CODE ${install_script}) - else (NOT WIN32) - set(PYTHONPATH "%HOMEDRIVE%\\Program Files\\ALPS\\lib;%HOMEDRIVE%\\Program Files (x86)\\ALPS\\lib") - set(PYTHONBIN "python") - configure_file(alpspython.bat.in ${PROJECT_BINARY_DIR}/tool/alpspython.bat) - install(PROGRAMS ${PROJECT_BINARY_DIR}/tool/alpspython.bat DESTINATION bin COMPONENT tools) - endif (NOT WIN32) - set(ALPSPYTHON_CONFIGURED TRUE) - endif(PYTHON_INTERPRETER) diff --git a/tutorials/code-07-mcmain-mcbase/CMakeLists.txt b/tutorials/code-07-mcmain-mcbase/CMakeLists.txt index 1de3e807f..9605e1330 100644 --- a/tutorials/code-07-mcmain-mcbase/CMakeLists.txt +++ b/tutorials/code-07-mcmain-mcbase/CMakeLists.txt @@ -24,22 +24,3 @@ if (MPI_FOUND) target_link_libraries(mpi_pscan ${ALPS_LIBRARIES}) endif (MPI_FOUND) - -if (ALPS_HAVE_PYTHON) - - # rule for generating python export - set_property(GLOBAL APPEND PROPERTY PY_MODULES_LIST ising_c) - if(BUILD_SHARED_LIBS) - add_library(ising_c MODULE ising.cpp export.cpp) - set_target_properties(ising_c PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - if(WIN32 AND NOT UNIX) - set_target_properties(ising_c PROPERTIES SUFFIX ".pyd") - endif(WIN32 AND NOT UNIX) - else(BUILD_SHARED_LIBS) - set_property(GLOBAL APPEND PROPERTY PY_STATIC_MODULES_LIST ising_c) - add_library(ising_c STATIC ising.cpp export.cpp) - endif (BUILD_SHARED_LIBS) - set_target_properties(ising_c PROPERTIES PREFIX "") - target_link_libraries(ising_c ${ALPS_LIBRARIES}) - -endif (ALPS_HAVE_PYTHON) diff --git a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/CMakeLists.txt b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/CMakeLists.txt index c76d63126..d7fe7617a 100644 --- a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/CMakeLists.txt +++ b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/CMakeLists.txt @@ -15,23 +15,3 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${bench_flags}") # rule for generating the heisenberg example program add_executable(heisenberg heisenberg.cpp) target_link_libraries(heisenberg ${ALPS_LIBRARIES}) - - -if (ALPS_HAVE_PYTHON) - - # rule for generating python export - set_property(GLOBAL APPEND PROPERTY PY_MODULES_LIST pyndsim) - if(BUILD_SHARED_LIBS) - add_library(pyndsim MODULE EXCLUDE_FROM_ALL export.cpp) - set_target_properties(pyndsim PROPERTIES COMPILE_DEFINITIONS "${ALPS_SHARED_CPPFLAGS}") - if(WIN32 AND NOT UNIX) - set_target_properties(pyndsim PROPERTIES SUFFIX ".pyd") - endif(WIN32 AND NOT UNIX) - else(BUILD_SHARED_LIBS) - set_property(GLOBAL APPEND PROPERTY PY_STATIC_MODULES_LIST pyndsim) - add_library(pyndsim STATIC EXCLUDE_FROM_ALL export.cpp) - endif (BUILD_SHARED_LIBS) - set_target_properties(pyndsim PROPERTIES PREFIX "") - target_link_libraries(pyndsim ${ALPS_LIBRARIES}) - -endif (ALPS_HAVE_PYTHON) From 8b1fb3a18de6141a264ae09b45f18ba643596c04 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 13:27:23 -0500 Subject: [PATCH 03/52] build: relocate pyalps project metadata --- .github/workflows/build_wheels.yml | 4 ++- README-py.md | 50 --------------------------- bindings/python/pyalps/CMakeLists.txt | 20 ++++++----- bindings/python/pyalps/LICENSE.txt | 19 ++++++++++ bindings/python/pyalps/README.md | 20 ++++++++--- bindings/python/pyalps/pyproject.toml | 32 +++++++++++++++++ pyproject.toml | 23 ------------ 7 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 README-py.md create mode 100644 bindings/python/pyalps/LICENSE.txt create mode 100644 bindings/python/pyalps/pyproject.toml delete mode 100644 pyproject.toml diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index fdf42bc3d..967a74f32 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -34,6 +34,8 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 + with: + package-dir: bindings/python/pyalps env: CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* CIBW_ARCHS: ${{ matrix.plat.arch }} @@ -97,7 +99,7 @@ jobs: - uses: actions/checkout@v7 - name: Build sdist - run: pipx run build --sdist + run: pipx run build --sdist --outdir dist bindings/python/pyalps - uses: actions/upload-artifact@v7 with: diff --git a/README-py.md b/README-py.md deleted file mode 100644 index 66a571a86..000000000 --- a/README-py.md +++ /dev/null @@ -1,50 +0,0 @@ -[![ALPS CI/CD](https://github.com/ALPSim/legacy/actions/workflows/build.yml/badge.svg)](https://github.com/ALPSim/legacy/actions/workflows/build.yml) - -## Python Algorithms and Libraries for Physics Simulations - -This is python packages for `Algorithms and Libraries for Physics Simulations` project. For more information check [README.txt](https://pypi.org/project/pyalps/2.3.3/README.txt). - -### Installation instruction from binaries - -1. pyALPS can be installed on most Linux and MacOS mcachines from prebuilt biniaries available on [PyPi](https://pypi.org/project/pyalps). -pyALPS can be installed using `pip` Python package manager: - -``` -pip install pyalps -``` - -### Installation instruction from sources - -1. Prerequisites - - CMake >= 3.22 - - Boost sources >= 1.76 - - BLAS/LAPACK - - HDF5 - - MPI - - Python >= 3.9 - - Python 3.13 requires Boost version 1.87 or later - - Earlier versions maybe also work but unsupported - - C++ compiler with C++17 support (CI covers GCC 11 through 15, Clang 14 through 22, and AppleClang) - - GNU Make or Ninja build system - -You need to download and unpack boost library: -``` -wget https://archives.boost.io/release/1.86.0/source/boost_1_86_0.tar.gz -tar -xzf boost_1_86_0.tar.gz -``` -Here we download `boost v1.86.0`, we have tested ALPS with versions `1.76.0` and `1.86.0`. - -2. Downloading and building sources -``` -git clone https://github.com/alpsim/ALPS ALPS -cd ALPS -Boost_SRC_DIR=`pwd`/../boost_1_86_0 python3 -m build --wheel -``` -This will download the most recent version of ALPS from the github repository, and build pyALPS python package. - -3. Installation - -Based on the version of the Python used to build pyALPS, the corresponding Python wheel will be created and stored in `dist` subdirectory. It can be installed using `pip`: -``` -pip install dist/pyalps-.whl -``` diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index f7db44eec..e56847456 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -22,6 +22,10 @@ list(PREPEND CMAKE_PREFIX_PATH "${_nanobind_cmake_dir}") find_package(nanobind 2.10 CONFIG REQUIRED) get_filename_component(_repo_root "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +set(_alps_source_root "${_repo_root}") +if(NOT EXISTS "${_alps_source_root}/tool/maxent.cpp") + set(_alps_source_root "${CMAKE_CURRENT_SOURCE_DIR}/_vendor") +endif() set(_bindings "${CMAKE_CURRENT_SOURCE_DIR}/cpp") link_directories(${ALPS_LIBRARY_DIRS}) @@ -67,19 +71,19 @@ nanobind_add_module(pyngsrandom01_c NB_STATIC "${_bindings}/ngs/random01.cpp") nanobind_add_module(pyngsaccumulator_c NB_STATIC "${_bindings}/ngs/accumulator.cpp") if(PYALPS_BUILD_APPLICATIONS) - if(NOT EXISTS "${_repo_root}/tool/maxent.cpp") + if(NOT EXISTS "${_alps_source_root}/tool/maxent.cpp") message(FATAL_ERROR - "PYALPS_BUILD_APPLICATIONS requires an ALPS source checkout. " + "PYALPS_BUILD_APPLICATIONS requires an ALPS source checkout or pyalps sdist. " "Configure with -DPYALPS_BUILD_APPLICATIONS=OFF for the core-only package.") endif() - set(_dmft "${_repo_root}/applications/dmft/qmc") + set(_dmft "${_alps_source_root}/applications/dmft/qmc") nanobind_add_module(maxent_c NB_STATIC - "${_repo_root}/tool/maxent.cpp" - "${_repo_root}/tool/maxent_helper.cpp" - "${_repo_root}/tool/maxent_simulation.cpp" - "${_repo_root}/tool/maxent_parms.cpp") + "${_alps_source_root}/tool/maxent.cpp" + "${_alps_source_root}/tool/maxent_helper.cpp" + "${_alps_source_root}/tool/maxent_simulation.cpp" + "${_alps_source_root}/tool/maxent_parms.cpp") nanobind_add_module(cthyb NB_STATIC "${_dmft}/hybridization/hybmain.cpp" @@ -116,7 +120,7 @@ if(PYALPS_BUILD_APPLICATIONS) target_compile_definitions(${_target} PRIVATE BUILD_PYTHON_MODULE) target_include_directories(${_target} PRIVATE "${_bindings}" "${_dmft}") endforeach() - target_include_directories(dwa_c PRIVATE "${_repo_root}/applications/qmc/dwa") + target_include_directories(dwa_c PRIVATE "${_alps_source_root}/applications/qmc/dwa") endif() foreach(_target IN LISTS _pyalps_targets) diff --git a/bindings/python/pyalps/LICENSE.txt b/bindings/python/pyalps/LICENSE.txt new file mode 100644 index 000000000..7a715fb42 --- /dev/null +++ b/bindings/python/pyalps/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright 2003-2025 ALPS Collaboration + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index defebb61a..7071f4e3d 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -1,9 +1,16 @@ # pyalps -Legacy-compatible Python bindings for ALPS, built as a standalone -`scikit-build-core` project using nanobind. The C++ ALPS library must be -built and installed separately; point `ALPS_DIR` at its `share/alps` -package directory when building this wheel. +Python applications and libraries for the Algorithms and Libraries for +Physics Simulations (ALPS) project. Binary wheels are available from PyPI: + +```sh +python -m pip install pyalps +``` + +The bindings are built as a standalone `scikit-build-core` project using +nanobind. A source build requires CMake 3.18 or newer, a C++17 compiler, +BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. Point `ALPS_DIR` at the +SDK's `share/alps` package directory. From the repository root: @@ -17,9 +24,12 @@ cmake --build _build/alps --target install ALPS_DIR="$PWD/_build/install/share/alps" \ CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" \ - python -m build --wheel + python -m build --wheel bindings/python/pyalps ``` +The wheel is written to `bindings/python/pyalps/dist` and can be installed +with `python -m pip install`. + `PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, DWA, CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake configuration for a smaller core-only developer build. diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml new file mode 100644 index 000000000..d2de3e273 --- /dev/null +++ b/bindings/python/pyalps/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["scikit-build-core>=1.0", "nanobind>=2.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "pyalps" +version = "2.3.4b1" +description = "Python Applications and Libraries for Physics Simulations" +readme = "README.md" +requires-python = ">=3.9" +license = "MIT" +dependencies = ["numpy", "scipy"] + +[tool.scikit-build] +cmake.source-dir = "." +wheel.packages = ["src/pyalps"] +wheel.license-files = ["LICENSE.txt"] +build.verbose = true + +[tool.scikit-build.cmake.define] +ALPS_DIR = { env = "ALPS_DIR" } + +# Application bindings compile selected legacy application sources. Preserve +# those sources when this subproject is distributed independently of the +# repository checkout so wheels can also be rebuilt from the sdist. +[tool.scikit-build.sdist.force-include] +"../../../applications/dmft/qmc" = "_vendor/applications/dmft/qmc" +"../../../applications/qmc/dwa" = "_vendor/applications/qmc/dwa" +"../../../tool" = "_vendor/tool" + +[tool.cibuildwheel] +manylinux-x86_64-image = "manylinux_2_28" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c8a0571c1..000000000 --- a/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[build-system] -requires = ["scikit-build-core>=0.10", "nanobind>=2.10"] -build-backend = "scikit_build_core.build" - -[project] -name = "pyalps" -version = "2.3.4b1" -description = "Python Applications and Libraries for Physics Simulations" -readme = "bindings/python/pyalps/README.md" -requires-python = ">=3.9" -license = "MIT" -dependencies = ["numpy", "scipy"] - -[tool.scikit-build] -cmake.source-dir = "bindings/python/pyalps" -wheel.packages = ["bindings/python/pyalps/src/pyalps"] -build.verbose = true - -[tool.scikit-build.cmake.define] -ALPS_DIR = { env = "ALPS_DIR" } - -[tool.cibuildwheel] -manylinux-x86_64-image = "manylinux_2_28" From 94b620822babc74b0207c2c74faf3fcb09bab980 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 13:47:34 -0500 Subject: [PATCH 04/52] build: retain macos 26 wheel coverage --- .github/workflows/build_wheels.yml | 1 + bindings/python/pyalps/LICENSE.txt | 19 ------------------- bindings/python/pyalps/pyproject.toml | 3 ++- 3 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 bindings/python/pyalps/LICENSE.txt diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 967a74f32..e3ba1ca5a 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -21,6 +21,7 @@ jobs: - { os: ubuntu-latest, target: "", arch: x86_64, homebrew: ''} #- { os: macos-13, target: "13.0" , arch: x86_64, homebrew: '/usr/local'} #DEPRECATED. Too old. - { os: macos-15, target: "15.0" , arch: arm64, homebrew: '/opt/homebrew'} + - { os: macos-26, target: "26.0" , arch: arm64, homebrew: '/opt/homebrew'} steps: - uses: actions/checkout@v7 diff --git a/bindings/python/pyalps/LICENSE.txt b/bindings/python/pyalps/LICENSE.txt deleted file mode 100644 index 7a715fb42..000000000 --- a/bindings/python/pyalps/LICENSE.txt +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 2003-2025 ALPS Collaboration - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index d2de3e273..f0403cc3e 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -14,7 +14,7 @@ dependencies = ["numpy", "scipy"] [tool.scikit-build] cmake.source-dir = "." wheel.packages = ["src/pyalps"] -wheel.license-files = ["LICENSE.txt"] +wheel.force-include = { "LICENSE.txt" = "${SKBUILD_METADATA_DIR}/licenses/LICENSE.txt" } build.verbose = true [tool.scikit-build.cmake.define] @@ -24,6 +24,7 @@ ALPS_DIR = { env = "ALPS_DIR" } # those sources when this subproject is distributed independently of the # repository checkout so wheels can also be rebuilt from the sdist. [tool.scikit-build.sdist.force-include] +"../../../LICENSE.txt" = "LICENSE.txt" "../../../applications/dmft/qmc" = "_vendor/applications/dmft/qmc" "../../../applications/qmc/dwa" = "_vendor/applications/qmc/dwa" "../../../tool" = "_vendor/tool" From 440bff8ce79a3bf72ad5e4943d81437fba7d55b6 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 13:59:31 -0500 Subject: [PATCH 05/52] fix: modernize pyalps Python compatibility --- .github/workflows/build_wheels.yml | 4 +- bindings/python/pyalps/CMakeLists.txt | 2 +- bindings/python/pyalps/README.md | 8 ++-- bindings/python/pyalps/pyproject.toml | 8 +++- bindings/python/pyalps/src/pyalps/__init__.py | 7 --- bindings/python/pyalps/src/pyalps/apptest.py | 6 +-- .../pyalps/src/pyalps/dict_intersect.py | 16 ++++--- bindings/python/pyalps/src/pyalps/ngs.py | 7 +-- bindings/python/pyalps/src/pyalps/tools.py | 22 +++++----- test/pyalps/test_binding_surface.py | 43 +++++++++++++++++++ 10 files changed, 81 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index e3ba1ca5a..9df18bb1e 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -34,11 +34,11 @@ jobs: restore-keys: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}- - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v3.4.1 with: package-dir: bindings/python/pyalps env: - CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* + CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* cp314-* CIBW_ARCHS: ${{ matrix.plat.arch }} CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} CIBW_ENVIRONMENT: > diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index e56847456..ba658bba9 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -11,7 +11,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(ALPS REQUIRED CONFIG) -find_package(Python 3.9 REQUIRED COMPONENTS Interpreter Development.Module) +find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) execute_process( COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 7071f4e3d..dbde7cdf7 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -7,10 +7,12 @@ Physics Simulations (ALPS) project. Binary wheels are available from PyPI: python -m pip install pyalps ``` +Install `pyalps[plot]` to use the Matplotlib plotting helpers. + The bindings are built as a standalone `scikit-build-core` project using -nanobind. A source build requires CMake 3.18 or newer, a C++17 compiler, -BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. Point `ALPS_DIR` at the -SDK's `share/alps` package directory. +nanobind. A source build requires Python 3.10 or newer, CMake 3.18 or newer, +a C++17 compiler, BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. Point +`ALPS_DIR` at the SDK's `share/alps` package directory. From the repository root: diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index f0403cc3e..1becb4ccb 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -7,9 +7,13 @@ name = "pyalps" version = "2.3.4b1" description = "Python Applications and Libraries for Physics Simulations" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = "MIT" -dependencies = ["numpy", "scipy"] +dependencies = ["numpy>=1.26", "scipy>=1.13"] + +[project.optional-dependencies] +plot = ["matplotlib>=3.8"] +test = ["pytest>=8"] [tool.scikit-build] cmake.source-dir = "." diff --git a/bindings/python/pyalps/src/pyalps/__init__.py b/bindings/python/pyalps/src/pyalps/__init__.py index f4b5c162b..29aefeb79 100644 --- a/bindings/python/pyalps/src/pyalps/__init__.py +++ b/bindings/python/pyalps/src/pyalps/__init__.py @@ -13,13 +13,6 @@ # **************************************************************************** import sys -import os.path -if sys.platform == 'darwin' and not os.path.exists(os.path.expanduser('~/.matplotlib/matplotlibrc')): - try: - import matplotlib - matplotlib.use('macosx') - except ImportError: - pass from .dataset import * from .tools import * diff --git a/bindings/python/pyalps/src/pyalps/apptest.py b/bindings/python/pyalps/src/pyalps/apptest.py index 5cf21c57c..b15c59a44 100644 --- a/bindings/python/pyalps/src/pyalps/apptest.py +++ b/bindings/python/pyalps/src/pyalps/apptest.py @@ -470,8 +470,9 @@ def checkProperties( testfile, reffile ): del tprop['filename'] del rprop['filename'] - if cmp(tprop, rprop) == 0: return True - else: return False + if tprop.keys() != rprop.keys(): + return False + return all(np.array_equal(tprop[key], rprop[key]) for key in tprop) def compareTest( testinputfile, outputs, tmpdir, tstart, compMethod='auto' ): @@ -783,4 +784,3 @@ def createTest( script, inputs=None, outputs=None, prefix=None, refdir='./ref' ) f.close() os.chmod(scriptname_prefixed, 0o755) - diff --git a/bindings/python/pyalps/src/pyalps/dict_intersect.py b/bindings/python/pyalps/src/pyalps/dict_intersect.py index a2c524d1d..2601acfb2 100644 --- a/bindings/python/pyalps/src/pyalps/dict_intersect.py +++ b/bindings/python/pyalps/src/pyalps/dict_intersect.py @@ -13,6 +13,12 @@ import numpy as np +def _values_equal(left, right): + try: + return bool(np.all(left == right)) + except (TypeError, ValueError): + return False + def dict_intersect(dicts): """ computes the intersection of a list of dicts @@ -27,12 +33,8 @@ def dict_intersect(dicts): take = True val0 = dicts[0][key] for idict in dicts: - try: - if val0 != idict[key]: - take = False - except: - if np.all(val0 != idict[key]): - take = False + if not _values_equal(val0, idict[key]): + take = False if take: ret[key] = dicts[0][key] return ret @@ -47,7 +49,7 @@ def dict_difference(dicts): take = True val0 = dicts[0][key] for idict in dicts: - if val0 != idict[key]: + if not _values_equal(val0, idict[key]): take = False if not take: ret.append(key) diff --git a/bindings/python/pyalps/src/pyalps/ngs.py b/bindings/python/pyalps/src/pyalps/ngs.py index cc3922687..9612a0149 100644 --- a/bindings/python/pyalps/src/pyalps/ngs.py +++ b/bindings/python/pyalps/src/pyalps/ngs.py @@ -12,12 +12,7 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -import sys - -if sys.version_info[:2] >= (3, 8): - from collections.abc import MutableMapping -else: - from collections import MutableMapping +from collections.abc import MutableMapping from .cxx.pyngsparams_c import params from .cxx.pyngsobservable_c import observable diff --git a/bindings/python/pyalps/src/pyalps/tools.py b/bindings/python/pyalps/src/pyalps/tools.py index 2f7e08922..84067d143 100644 --- a/bindings/python/pyalps/src/pyalps/tools.py +++ b/bindings/python/pyalps/src/pyalps/tools.py @@ -21,6 +21,7 @@ import sys import glob from . import math +import numpy as np import scipy.stats import copy @@ -216,7 +217,7 @@ def evaluateQWL(infiles, appname='qwl_evaluate', DELTA_T=None, T_MIN=None, T_MAX cmdline += make_list(infiles) res = executeCommand(cmdline) if res != 0: - raise Excpetion("Execution error in evaluateQWL: " + str(res)) + raise RuntimeError("Execution error in evaluateQWL: " + str(res)) datasets = [] for infile in infiles: datasets.append([]) @@ -551,9 +552,9 @@ def checkSteadyState(sets=None, outfile=None, observable=None, confidenceInterva else: ts = pyalps.loadTimeSeries(outfile, observable); ### y N = ts.size; - idx = scipy.linspace(1, N, N); ### x + idx = np.linspace(1, N, N); ### x - beta1 = scipy.polyfit(idx, ts, 1)[0]; ### slope + beta1 = np.polyfit(idx, ts, 1)[0]; ### slope ts_std = np.std(ts, ddof=1); ### unbiased estimate of standard deviation in y beta1_std = math.sqrt((12.*ts_std*ts_std)/(N * (N*N-1))); ### unbiased estimate of standard deviation in slope @@ -837,7 +838,7 @@ def stringListToList(inList): #find number of bracketed items (they come in pairs) numbrackets=dum.count('[') if numbrackets==0 : - unbracketed=map(float,dum.replace('[','').replace(']','').replace(' ','').split(',')) + unbracketed=list(map(float,dum.replace('[','').replace(']','').replace(' ','').split(','))) for q in unbracketed: outList.append([q]) elif numbrackets>0: @@ -847,16 +848,16 @@ def stringListToList(inList): startInd=dum.find('[',count) finishInd=dum.find(']', count) if startInd>count: - unbracketed=map(float,(dum[count:startInd-1].replace('[','').replace(']','')\ - .replace(' ','').split(','))) + unbracketed=list(map(float,(dum[count:startInd-1].replace('[','').replace(']','')\ + .replace(' ','').split(',')))) for q in unbracketed: outList.append([q]) - outList.append(map(float,dum[startInd:finishInd+1].replace('[','').\ - replace(']','').replace(' ','').split(','))) + outList.append(list(map(float,dum[startInd:finishInd+1].replace('[','').\ + replace(']','').replace(' ','').split(',')))) count=finishInd+2 if len(dum)-count>0: - unbracketed=map(float,dum[count:len(dum)].replace('[','').replace(']','')\ - .replace(' ','').split(',')) + unbracketed=list(map(float,dum[count:len(dum)].replace('[','').replace(']','')\ + .replace(' ','').split(','))) for q in unbracketed: outList.append([q]) else: @@ -1050,4 +1051,3 @@ def CycleMarkers (data, foreach, q.props['line'] = all[key] + '-' return data - diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index fd62e4063..091913c22 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -10,6 +10,7 @@ import importlib import os import tempfile +from types import SimpleNamespace import numpy as np @@ -138,6 +139,48 @@ def test_optional_application_extension_surface(): assert module.__name__.endswith(name) +def test_current_python_numpy_and_scipy_compatibility(monkeypatch): + import pyalps + import pyalps.dwa as dwa + + assert callable(dwa.thermalized) + + parsed = pyalps.stringListToList("[1,[2,3],4]") + assert parsed == [[1.0], [2.0, 3.0], [4.0]] + + shared = pyalps.dict_intersect([ + {"array": np.array([1, 2]), "scalar": 3}, + {"array": np.array([1, 2]), "scalar": 3}, + ]) + np.testing.assert_array_equal(shared["array"], [1, 2]) + assert shared["scalar"] == 3 + + monkeypatch.setattr( + pyalps, + "loadTimeSeries", + lambda *_args: np.array([1.0, 1.1, 0.9, 1.0]), + ) + steady = pyalps.checkSteadyState(outfile="unused.h5", observable="energy") + assert isinstance(steady["value"], (bool, np.bool_)) + + +def test_python3_property_comparison(monkeypatch): + import pyalps + import pyalps.apptest as apptest + + properties = { + "test.h5": {"vector": np.array([1, 2]), "value": 3}, + "reference.h5": {"vector": np.array([1, 2]), "value": 3}, + } + + class Loader: + def GetProperties(self, filenames): + return [SimpleNamespace(props=properties[filenames[0]].copy())] + + monkeypatch.setattr(pyalps.load, "Hdf5Loader", Loader) + assert apptest.checkProperties("test.h5", "reference.h5") + + if __name__ == "__main__": for test in ( test_extension_import_surface, From 687253f638d35dec305774511e7c602abe441a5d Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 15:51:10 -0500 Subject: [PATCH 06/52] fix: resolve wheel ALPS_DIR via $(pwd) in cibuildwheel env CIBW_ENVIRONMENT does not expand the {project} placeholder (only before-all/before-build/test/repair commands do), so ALPS_DIR and CCACHE_DIR were set to the literal string "{project}/...". The wheel build's find_package(ALPS REQUIRED CONFIG) then could not locate the ALPSConfig.cmake installed by CIBW_BEFORE_ALL, failing CMake configure. Use $(pwd), which cibuildwheel evaluates in the build environment (cwd=/project in the Linux container, repo root on macOS) to the same directory where _build/cibw-install lives. Validated end-to-end with a local manylinux_2_28_aarch64 build: wheel builds, repairs, 14 tests pass. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build_wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 9df18bb1e..3dbb28f68 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -42,8 +42,8 @@ jobs: CIBW_ARCHS: ${{ matrix.plat.arch }} CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} CIBW_ENVIRONMENT: > - ALPS_DIR={project}/_build/cibw-install/share/alps - CCACHE_DIR={project}/_build/ccache + ALPS_DIR=$(pwd)/_build/cibw-install/share/alps + CCACHE_DIR=$(pwd)/_build/ccache CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" CIBW_BEFORE_ALL_LINUX: > @@ -72,8 +72,8 @@ jobs: -DHDF5_ROOT=${{ matrix.plat.homebrew }}/opt/hdf5 && cmake --build {project}/_build/cibw-alps --target install -j2 CIBW_ENVIRONMENT_MACOS: > - ALPS_DIR={project}/_build/cibw-install/share/alps - CCACHE_DIR={project}/_build/ccache + ALPS_DIR=$(pwd)/_build/cibw-install/share/alps + CCACHE_DIR=$(pwd)/_build/ccache CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }} From 6a4104b5b8aebf6f10045e274d3154c62497ecb4 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 22 Jul 2026 16:42:07 -0500 Subject: [PATCH 07/52] fix: build pyalps musllinux wheels via libtirpc XDR musllinux (Alpine/musl) lacks dnf, glibc's SunRPC/XDR, and execinfo. Branch before_all to apk; install libtirpc for ALPS's system-XDR path (ALPS_HAVE_RPC_XDR_H) and disable the execinfo backtrace. Validated end-to-end on musllinux_1_2_aarch64: wheel builds, auditwheel bundles libtirpc, 14/14 tests pass. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build_wheels.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 3dbb28f68..4d331c15a 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -46,8 +46,18 @@ jobs: CCACHE_DIR=$(pwd)/_build/ccache CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + # manylinux (AlmaLinux/glibc) uses dnf and glibc's SunRPC/XDR + execinfo. + # musllinux (Alpine/musl) has neither: install libtirpc for the system + # XDR path (ALPS_HAVE_RPC_XDR_H) and disable the execinfo backtrace. CIBW_BEFORE_ALL_LINUX: > - dnf install -y ccache cmake hdf5-devel lapack-devel ninja-build && + if command -v dnf >/dev/null 2>&1; + then dnf install -y ccache cmake hdf5-devel lapack-devel ninja-build; EXTRA=; + else apk add --no-cache ccache ninja-is-really-ninja hdf5-dev lapack-dev libtirpc-dev; + ln -sf /usr/include/tirpc/rpc /usr/include/rpc; + ln -sf /usr/include/tirpc/netconfig.h /usr/include/netconfig.h; + export CXXFLAGS="-DALPS_NGS_NO_STACKTRACE"; + EXTRA="-DALPS_HAVE_RPC_XDR_H=1 -DCMAKE_CXX_STANDARD_LIBRARIES=-ltirpc"; + fi && cmake -S {project} -B {project}/_build/cibw-alps -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install @@ -56,7 +66,8 @@ jobs: -DALPS_BUILD_TESTS=OFF -DALPS_BUILD_EXAMPLES=OFF -DALPS_BUILD_APPLICATIONS=OFF - -DALPS_ENABLE_MPI=OFF && + -DALPS_ENABLE_MPI=OFF + $EXTRA && cmake --build {project}/_build/cibw-alps --target install -j2 CIBW_BEFORE_ALL_MACOS: > brew install ccache cmake hdf5 ninja && From 07e93d706f5b68ed99370b515b087e13d204356e Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 03:37:18 -0500 Subject: [PATCH 08/52] fix: restore pyalea mcanalyze bindings The nanobind migration left the mcanalyze free functions unbound even though pyalps.alea still dispatches to them through alea_detail: autocorrelation_distance/_limit, cut_head_distance/_limit, cut_tail_distance/_limit, exponential_autocorrelation_time_distance/ _limit, uncorrelated_error, and binning_error all raised AttributeError at call time. Restore them with the same instantiation set as the Boost.Python module (scalar and vector mcdata/mctimeseries/ mctimeseries_view where each was previously exposed). The exponential fit helpers return StdPairDouble so the documented fit.first / fit.second attribute API keeps working, and integrated_autocorrelation_time now accepts that StdPairDouble as well as a plain 2-tuple, as its comment already promised. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/cpp/pyalea.cpp | 84 +++++++++++++++++++ .../python/pyalps/src/pyalps/pyalps_config.py | 2 - 2 files changed, 84 insertions(+), 2 deletions(-) delete mode 100644 bindings/python/pyalps/src/pyalps/pyalps_config.py diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp index b5ecd5607..fb9b2b86c 100644 --- a/bindings/python/pyalps/cpp/pyalea.cpp +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -132,6 +132,14 @@ template nb::object variance_vector(T const & x) { return seq_to_numpy(alps::alea::variance(x)); } +template +nb::object uncorrelated_error_vector(T const & x) { + return seq_to_numpy(alps::alea::uncorrelated_error(x)); +} +template +nb::object binning_error_vector(T const & x) { + return seq_to_numpy(alps::alea::binning_error(x)); +} // mctimeseries.timeseries() returns std::vector; hand // back to Python as numpy. For scalar ValueType we pack 1-D; for // vector ValueType we pack 2-D. mctimeseries_view has the @@ -365,4 +373,80 @@ NB_MODULE(pyalea_c, m) { static_cast (*)(alps::alea::mctimeseries_view const &)>( &alps::alea::reverse_running_mean)); #undef DEF_ALL + // ─── mcanalyze free functions consumed by pyalps.alea ──────────── + // + // autocorrelation / cut_head / cut_tail / error in alea.py dispatch + // to these through alea_detail; the instantiation set matches the + // Boost.Python module. + #define DEF_TS_SCALAR(name, fn) \ + m.def(name, &fn>); \ + m.def(name, &fn>); \ + m.def(name, &fn>); + #define DEF_TS_VECTOR(name, fn) \ + m.def(name, &fn>>); \ + m.def(name, &fn>>); \ + m.def(name, &fn>>); + // autocorrelation — by distance or by decay limit. + DEF_TS_SCALAR("autocorrelation_distance", alps::alea::autocorrelation_distance) + DEF_TS_VECTOR("autocorrelation_distance", alps::alea::autocorrelation_distance) + DEF_TS_SCALAR("autocorrelation_limit", alps::alea::autocorrelation_limit) + DEF_TS_VECTOR("autocorrelation_limit", alps::alea::autocorrelation_limit) + // head/tail cuts — views by distance for scalar and vector series; + // by decay limit for scalar series only. + DEF_TS_SCALAR("cut_head_distance", alps::alea::cut_head_distance) + DEF_TS_VECTOR("cut_head_distance", alps::alea::cut_head_distance) + DEF_TS_SCALAR("cut_tail_distance", alps::alea::cut_tail_distance) + DEF_TS_VECTOR("cut_tail_distance", alps::alea::cut_tail_distance) + DEF_TS_SCALAR("cut_head_limit", alps::alea::cut_head_limit) + DEF_TS_SCALAR("cut_tail_limit", alps::alea::cut_tail_limit) + // error estimates — scalar overloads return float, vector overloads numpy. + DEF_TS_SCALAR("uncorrelated_error", alps::alea::uncorrelated_error) + DEF_TS_SCALAR("binning_error", alps::alea::binning_error) + m.def("uncorrelated_error", &uncorrelated_error_vector>>); + m.def("uncorrelated_error", &uncorrelated_error_vector>>); + m.def("uncorrelated_error", &uncorrelated_error_vector>>); + m.def("binning_error", &binning_error_vector>>); + m.def("binning_error", &binning_error_vector>>); + m.def("binning_error", &binning_error_vector>>); + #undef DEF_TS_SCALAR + #undef DEF_TS_VECTOR + // exponential_autocorrelation_time fits — return StdPairDouble so the + // fit.first / fit.second attribute API documented in pyalps.alea + // is preserved. + m.def("exponential_autocorrelation_time_distance", + [](alps::alea::mctimeseries const & ts, int from, int to) { + std::pair fit = + alps::alea::exponential_autocorrelation_time_distance(ts, from, to); + return StdPairDouble(fit.first, fit.second); + }); + m.def("exponential_autocorrelation_time_distance", + [](alps::alea::mctimeseries_view const & ts, int from, int to) { + std::pair fit = + alps::alea::exponential_autocorrelation_time_distance(ts, from, to); + return StdPairDouble(fit.first, fit.second); + }); + m.def("exponential_autocorrelation_time_limit", + [](alps::alea::mctimeseries const & ts, double max, double min) { + std::pair fit = + alps::alea::exponential_autocorrelation_time_limit(ts, max, min); + return StdPairDouble(fit.first, fit.second); + }); + m.def("exponential_autocorrelation_time_limit", + [](alps::alea::mctimeseries_view const & ts, double max, double min) { + std::pair fit = + alps::alea::exponential_autocorrelation_time_limit(ts, max, min); + return StdPairDouble(fit.first, fit.second); + }); + // integrated_autocorrelation_time also accepts the StdPairDouble + // returned by the fit helpers, in addition to a plain 2-tuple. + m.def("integrated_autocorrelation_time", + [](alps::alea::mctimeseries const & ts, StdPairDouble const & fit) { + return alps::alea::integrated_autocorrelation_time( + ts, std::pair(fit.first, fit.second)); + }); + m.def("integrated_autocorrelation_time", + [](alps::alea::mctimeseries_view const & ts, StdPairDouble const & fit) { + return alps::alea::integrated_autocorrelation_time( + ts, std::pair(fit.first, fit.second)); + }); } diff --git a/bindings/python/pyalps/src/pyalps/pyalps_config.py b/bindings/python/pyalps/src/pyalps/pyalps_config.py deleted file mode 100644 index 50a06b6f4..000000000 --- a/bindings/python/pyalps/src/pyalps/pyalps_config.py +++ /dev/null @@ -1,2 +0,0 @@ -ALPS_XML_INSTALL_DIR="" -ALPS_BIN_INSTALL_DIR="" \ No newline at end of file From c1a4a2614fdf37fbe87ebbd0bf76054da3d3cc29 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 03:37:18 -0500 Subject: [PATCH 09/52] fix: bundle ALPS xml library in pyalps packages The standalone wheel dropped the XML/XSL data path: pyalps_config.py was checked in with hardcoded-empty install dirs and its template was never configured, and the wheel shipped no pyalps/xml directory, so pyalps.tools stylesheet and lattice/model-library workflows regressed against the legacy wheel build. Install the stylesheets plus the lattice and model libraries into pyalps/xml as the legacy ALPS_PYTHON_WHEEL build did, generate pyalps_config.py at build time with fallback paths pointing at the ALPS SDK the build used, drop the stale checked-in copy, and vendor lib/xml into the sdist so wheels rebuilt from it bundle the same files. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/CMakeLists.txt | 28 +++++++++++++++++++ bindings/python/pyalps/pyproject.toml | 11 ++++++-- .../pyalps/src/pyalps/pyalps_config.py.in | 4 +-- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index ba658bba9..55d9bdb24 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -141,3 +141,31 @@ install(TARGETS ${_pyalps_targets} install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/" DESTINATION pyalps FILES_MATCHING PATTERN "*.py") + +# Runtime fallback paths pointing at the ALPS SDK this build used. The +# in-package pyalps/xml and pyalps/bin directories take precedence in +# pyalps.tools when they exist. +set(PYALPS_ALPS_ROOT "${ALPS_ROOT_DIR}") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/pyalps_config.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" DESTINATION pyalps) + +# The ALPS XML/XSL library ships inside the package (pyalps/xml), as the +# legacy wheel build did: the stylesheets plus the lattice and model +# libraries that parameter files reference by default. +set(_alps_xml_source "${_alps_source_root}/lib/xml") +if(NOT EXISTS "${_alps_xml_source}/ALPS.xsl") + message(FATAL_ERROR + "ALPS XML stylesheets not found at ${_alps_xml_source}. " + "They are required so the pyalps package can bundle pyalps/xml.") +endif() +install(DIRECTORY "${_alps_xml_source}/" DESTINATION pyalps/xml + FILES_MATCHING PATTERN "*.xsl") +configure_file("${_alps_xml_source}/lattices.xml.in" + "${CMAKE_CURRENT_BINARY_DIR}/xml/lattices.xml" COPYONLY) +configure_file("${_alps_xml_source}/models.xml.in" + "${CMAKE_CURRENT_BINARY_DIR}/xml/models.xml" COPYONLY) +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/xml/lattices.xml" + "${CMAKE_CURRENT_BINARY_DIR}/xml/models.xml" + DESTINATION pyalps/xml) diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index 1becb4ccb..44f93b850 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -19,19 +19,24 @@ test = ["pytest>=8"] cmake.source-dir = "." wheel.packages = ["src/pyalps"] wheel.force-include = { "LICENSE.txt" = "${SKBUILD_METADATA_DIR}/licenses/LICENSE.txt" } +# pyalps_config.py is generated by CMake from this template; the template +# itself does not belong in the wheel. +wheel.exclude = ["pyalps/pyalps_config.py.in"] build.verbose = true [tool.scikit-build.cmake.define] ALPS_DIR = { env = "ALPS_DIR" } -# Application bindings compile selected legacy application sources. Preserve -# those sources when this subproject is distributed independently of the -# repository checkout so wheels can also be rebuilt from the sdist. +# Application bindings compile selected legacy application sources, and the +# package bundles the ALPS XML/XSL library. Preserve both when this subproject +# is distributed independently of the repository checkout so wheels can also +# be rebuilt from the sdist. [tool.scikit-build.sdist.force-include] "../../../LICENSE.txt" = "LICENSE.txt" "../../../applications/dmft/qmc" = "_vendor/applications/dmft/qmc" "../../../applications/qmc/dwa" = "_vendor/applications/qmc/dwa" "../../../tool" = "_vendor/tool" +"../../../lib/xml" = "_vendor/lib/xml" [tool.cibuildwheel] manylinux-x86_64-image = "manylinux_2_28" diff --git a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in index 150ef0f5e..111f88995 100644 --- a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in +++ b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in @@ -1,2 +1,2 @@ -ALPS_XML_INSTALL_DIR="@CMAKE_INSTALL_PREFIX@/lib/xml" -ALPS_BIN_INSTALL_DIR="@CMAKE_INSTALL_PREFIX@/bin" +ALPS_XML_INSTALL_DIR="@PYALPS_ALPS_ROOT@/lib/xml" +ALPS_BIN_INSTALL_DIR="@PYALPS_ALPS_ROOT@/bin" From cabdd87594de44faabb12b6c4968db06f860a90f Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 03:37:18 -0500 Subject: [PATCH 10/52] test: cover restored mcanalyze surface and packaged xml Exercise the full pyalps.alea entry-point surface (autocorrelation, head/tail cuts, exponential fit attributes, integrated autocorrelation time from both StdPairDouble and tuple, uncorrelated and binning errors for scalar and vector series) and assert the installed package resolves ALPS.xsl and ships the lattice and model libraries. Co-Authored-By: Claude Fable 5 --- test/pyalps/test_binding_surface.py | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 091913c22..bfcc35759 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -104,6 +104,72 @@ def test_alea_numpy_and_mcdata_operators(): assert duplicate.error == total.error +def test_alea_mcanalyze_surface(): + from pyalps import alea + from pyalps.cxx.pyalea_c import ( + MCScalarTimeseries, + MCScalarTimeseriesView, + MCVectorTimeseries, + StdPairDouble, + integrated_autocorrelation_time, + size, + ) + from pyalps.cxx.pytools_c import rng + + generator = rng(42) + samples = [] + state = 0.0 + for _ in range(512): + state = 0.9 * state + 0.1 * (2 * generator() - 1) + samples.append(state) + series = MCScalarTimeseries(np.asarray(samples)) + + correlation = alea.autocorrelation(series, _distance=16) + assert isinstance(correlation, MCScalarTimeseries) + assert size(correlation) == 16 + limited = alea.autocorrelation(series, _limit=0.2) + assert size(limited) >= 1 + + head = alea.cut_head(series, _distance=100) + tail = alea.cut_tail(series, _distance=100) + assert isinstance(head, MCScalarTimeseriesView) + assert size(head) == 412 + assert size(tail) == 412 + assert size(alea.cut_head(correlation, _limit=0.5)) < 16 + + fit = alea.exponential_autocorrelation_time(correlation, _from=1, _to=8) + assert isinstance(fit, StdPairDouble) + assert fit.second < 0 # decaying autocorrelation + ranged = alea.exponential_autocorrelation_time(correlation, _max=0.8, _min=0.2) + assert isinstance(ranged, StdPairDouble) + + tau_from_pair = integrated_autocorrelation_time(correlation, fit) + tau_from_tuple = integrated_autocorrelation_time(correlation, (fit.first, fit.second)) + assert tau_from_pair == tau_from_tuple + assert tau_from_pair > 0 + + assert alea.error(series) > 0 + assert alea.error(series, "binning") > 0 + + vector_series = MCVectorTimeseries(np.asarray([[float(i + j) for j in range(3)] for i in range(64)])) + vector_error = alea.error(vector_series) + assert vector_error.shape == (3,) + assert np.all(vector_error > 0) + vector_correlation = alea.autocorrelation(vector_series, _distance=4) + assert vector_correlation.timeseries().shape == (4, 3) + + +def test_packaged_xml_stylesheets(): + import pyalps.tools + + xsl = pyalps.tools.xslPath() + assert os.path.basename(xsl) == "ALPS.xsl" + assert os.path.exists(xsl) + xml_dir = os.path.dirname(xsl) + for name in ("lattices.xml", "models.xml", "plot2mpl.xsl"): + assert os.path.exists(os.path.join(xml_dir, name)) + + def test_ngs_observable_containers(): from pyalps import ngs @@ -186,6 +252,8 @@ def GetProperties(self, filenames): test_extension_import_surface, test_cross_module_parameter_archive_and_rng_roundtrip, test_alea_numpy_and_mcdata_operators, + test_alea_mcanalyze_surface, + test_packaged_xml_stylesheets, test_ngs_observable_containers, test_name_encoding_roundtrip, test_accumulator_surface, From 72b1c5b5314717636999fea10eab283b011bbe94 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 10:54:43 -0500 Subject: [PATCH 11/52] fix: restore params parameter-file constructor The Boost.Python module exposed params(str), which reads a classic ALPS text parameter file through alps::params(boost::filesystem::path). The nanobind module only kept the default, dict, and archive constructors. Restore the filename form and cover it in the binding-surface test. Found by a symbol-level audit of the old module exports against the built nanobind modules; this was the only genuinely dropped entry point remaining (commented-out registrations in the legacy sources such as convert2numpy and the createSigned*/createSimple* factories were never exported by the old build). Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/cpp/ngs/params.cpp | 8 ++++++++ test/pyalps/test_binding_surface.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index f6d9c8957..0994346f9 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,13 @@ NB_MODULE(pyngsparams_c, m) { new (self) alps::params(py_dict_to_params(d)); }, nb::arg("dict")) + // Read a classic ALPS text parameter file, matching the str + // constructor of the Boost.Python module. + .def("__init__", + [](alps::params * self, std::string const & filename) { + new (self) alps::params(boost::filesystem::path(filename)); + }, + nb::arg("filename")) .def(nb::init(), nb::arg("archive"), nb::arg("path") = std::string("/parameters")) diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index bfcc35759..0d48df969 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -104,6 +104,19 @@ def test_alea_numpy_and_mcdata_operators(): assert duplicate.error == total.error +def test_params_from_parameter_file(): + from pyalps.cxx.pyngsparams_c import params + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "input.parm") + with open(path, "w") as parameter_file: + parameter_file.write('LATTICE="chain lattice";\nL=10;\nT=2.25;\n') + loaded = params(path) + assert str(loaded["LATTICE"]) == "chain lattice" + assert int(loaded["L"]) == 10 + assert float(loaded["T"]) == 2.25 + + def test_alea_mcanalyze_surface(): from pyalps import alea from pyalps.cxx.pyalea_c import ( @@ -251,6 +264,7 @@ def GetProperties(self, filenames): for test in ( test_extension_import_surface, test_cross_module_parameter_archive_and_rng_roundtrip, + test_params_from_parameter_file, test_alea_numpy_and_mcdata_operators, test_alea_mcanalyze_surface, test_packaged_xml_stylesheets, From f8eb9341ab5d9c9d29f7f2e4e11f1e5c156b94e5 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 12:24:20 -0500 Subject: [PATCH 12/52] build: drive the wheel SDK build through a CMake preset The libs-only ALPS SDK configure was spelled out three times with drifting variations: CIBW_BEFORE_ALL_LINUX, CIBW_BEFORE_ALL_MACOS, and the developer instructions in the pyalps README. Capture it once as the wheel-deps configure/build preset (building on the CMakePresets.json introduced by the root-layout consolidation) and invoke the preset from all three places. Platform-specific extras stay on the command line: ccache launcher and musl XDR flags in CI, HDF5_ROOT now resolved via brew --prefix instead of a hard-coded matrix key. Co-Authored-By: Claude Fable 5 --- .github/workflows/build_wheels.yml | 43 ++++++++++++------------------ CMakePresets.json | 21 +++++++++++++++ bindings/python/pyalps/README.md | 25 +++++++++-------- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 4d331c15a..f0acb9fde 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -17,11 +17,11 @@ jobs: strategy: matrix: # macos-13 is an intel runner, macos-14 is apple silicon - plat: - - { os: ubuntu-latest, target: "", arch: x86_64, homebrew: ''} - #- { os: macos-13, target: "13.0" , arch: x86_64, homebrew: '/usr/local'} #DEPRECATED. Too old. - - { os: macos-15, target: "15.0" , arch: arm64, homebrew: '/opt/homebrew'} - - { os: macos-26, target: "26.0" , arch: arm64, homebrew: '/opt/homebrew'} + plat: + - { os: ubuntu-latest, target: "", arch: x86_64 } + #- { os: macos-13, target: "13.0" , arch: x86_64 } #DEPRECATED. Too old. + - { os: macos-15, target: "15.0" , arch: arm64 } + - { os: macos-26, target: "26.0" , arch: arm64 } steps: - uses: actions/checkout@v7 @@ -42,10 +42,13 @@ jobs: CIBW_ARCHS: ${{ matrix.plat.arch }} CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} CIBW_ENVIRONMENT: > - ALPS_DIR=$(pwd)/_build/cibw-install/share/alps + ALPS_DIR=$(pwd)/_build/wheel-deps/install/share/alps CCACHE_DIR=$(pwd)/_build/ccache CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + # The SDK configuration lives in the wheel-deps preset in + # CMakePresets.json; only per-platform dependency setup and extra + # cache entries are spelled out here. # manylinux (AlmaLinux/glibc) uses dnf and glibc's SunRPC/XDR + execinfo. # musllinux (Alpine/musl) has neither: install libtirpc for the system # XDR path (ALPS_HAVE_RPC_XDR_H) and disable the execinfo backtrace. @@ -58,32 +61,20 @@ jobs: export CXXFLAGS="-DALPS_NGS_NO_STACKTRACE"; EXTRA="-DALPS_HAVE_RPC_XDR_H=1 -DCMAKE_CXX_STANDARD_LIBRARIES=-ltirpc"; fi && - cmake -S {project} -B {project}/_build/cibw-alps -G Ninja - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install + cd {project} && + cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - -DALPS_BUILD_LIBS_ONLY=ON - -DALPS_BUILD_TESTS=OFF - -DALPS_BUILD_EXAMPLES=OFF - -DALPS_BUILD_APPLICATIONS=OFF - -DALPS_ENABLE_MPI=OFF $EXTRA && - cmake --build {project}/_build/cibw-alps --target install -j2 + cmake --build --preset wheel-deps -j2 CIBW_BEFORE_ALL_MACOS: > brew install ccache cmake hdf5 ninja && - cmake -S {project} -B {project}/_build/cibw-alps -G Ninja - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX={project}/_build/cibw-install + cd {project} && + cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - -DALPS_BUILD_LIBS_ONLY=ON - -DALPS_BUILD_TESTS=OFF - -DALPS_BUILD_EXAMPLES=OFF - -DALPS_BUILD_APPLICATIONS=OFF - -DALPS_ENABLE_MPI=OFF - -DHDF5_ROOT=${{ matrix.plat.homebrew }}/opt/hdf5 && - cmake --build {project}/_build/cibw-alps --target install -j2 + -DHDF5_ROOT=$(brew --prefix hdf5) && + cmake --build --preset wheel-deps -j2 CIBW_ENVIRONMENT_MACOS: > - ALPS_DIR=$(pwd)/_build/cibw-install/share/alps + ALPS_DIR=$(pwd)/_build/wheel-deps/install/share/alps CCACHE_DIR=$(pwd)/_build/ccache CCACHE_NAMESPACE=pyalps-wheel CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" diff --git a/CMakePresets.json b/CMakePresets.json index 512f514ea..288d282e9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,12 +13,33 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "wheel-deps", + "displayName": "ALPS C++ SDK for pyalps wheels", + "description": "Libs-only SDK install that the standalone pyalps wheel build links against (ALPS_DIR=_build/wheel-deps/install/share/alps).", + "generator": "Ninja", + "binaryDir": "${sourceDir}/_build/wheel-deps", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/_build/wheel-deps/install", + "ALPS_BUILD_LIBS_ONLY": "ON", + "ALPS_BUILD_TESTS": "OFF", + "ALPS_BUILD_EXAMPLES": "OFF", + "ALPS_BUILD_APPLICATIONS": "OFF", + "ALPS_ENABLE_MPI": "OFF" + } } ], "buildPresets": [ { "name": "default", "configurePreset": "default" + }, + { + "name": "wheel-deps", + "configurePreset": "wheel-deps", + "targets": ["install"] } ], "testPresets": [ diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index dbde7cdf7..26b045750 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -10,27 +10,26 @@ python -m pip install pyalps Install `pyalps[plot]` to use the Matplotlib plotting helpers. The bindings are built as a standalone `scikit-build-core` project using -nanobind. A source build requires Python 3.10 or newer, CMake 3.18 or newer, -a C++17 compiler, BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. Point -`ALPS_DIR` at the SDK's `share/alps` package directory. +nanobind. A source build requires Python 3.10 or newer, CMake 3.21 or newer, +Ninja, a C++17 compiler, BLAS/LAPACK, HDF5, and an installed ALPS C++ SDK. +Point `ALPS_DIR` at the SDK's `share/alps` package directory. +The `wheel-deps` CMake preset builds the SDK exactly as the wheel CI does. From the repository root: ```sh -cmake -S . -B _build/alps -G Ninja \ - -DCMAKE_INSTALL_PREFIX="$PWD/_build/install" \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ - -DALPS_ENABLE_MPI=OFF \ - -DALPS_BUILD_LIBS_ONLY=ON -cmake --build _build/alps --target install - -ALPS_DIR="$PWD/_build/install/share/alps" \ - CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" \ +cmake --preset wheel-deps +cmake --build --preset wheel-deps + +ALPS_DIR="$PWD/_build/wheel-deps/install/share/alps" \ python -m build --wheel bindings/python/pyalps ``` The wheel is written to `bindings/python/pyalps/dist` and can be installed -with `python -m pip install`. +with `python -m pip install`. With ccache installed, configure with +`cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache` and set +`CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache"` for the wheel build to +speed up rebuilds. `PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, DWA, CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake From 60d040dec71b29b80604942a71c4c5627d461134 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 12:27:23 -0500 Subject: [PATCH 13/52] build: move static cibuildwheel config into pyproject The workflow carried the full cibuildwheel configuration as CIBW_* env vars, duplicating the shared environment block for macOS just to add two entries. Keep the static configuration in [tool.cibuildwheel] next to the package (where the manylinux image already lived) so local cibuildwheel runs match CI, and express the macOS-only CXXFLAGS as an inherit/append override instead of a copy of the common block. The workflow now sets only matrix-derived values: CIBW_ARCHS and MACOSX_DEPLOYMENT_TARGET, which cibuildwheel reads from the host environment. Co-Authored-By: Claude Fable 5 --- .github/workflows/build_wheels.yml | 53 ++------------------------- bindings/python/pyalps/pyproject.toml | 44 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index f0acb9fde..a23e5e591 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -38,56 +38,11 @@ jobs: with: package-dir: bindings/python/pyalps env: - CIBW_BUILD: cp310-* cp311-* cp312-* cp313-* cp314-* + # The wheel build is configured in [tool.cibuildwheel] in + # bindings/python/pyalps/pyproject.toml and the wheel-deps preset in + # CMakePresets.json; only matrix-derived values live here. CIBW_ARCHS: ${{ matrix.plat.arch }} - CIBW_ARCHS_MACOS: ${{ matrix.plat.arch }} - CIBW_ENVIRONMENT: > - ALPS_DIR=$(pwd)/_build/wheel-deps/install/share/alps - CCACHE_DIR=$(pwd)/_build/ccache - CCACHE_NAMESPACE=pyalps-wheel - CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" - # The SDK configuration lives in the wheel-deps preset in - # CMakePresets.json; only per-platform dependency setup and extra - # cache entries are spelled out here. - # manylinux (AlmaLinux/glibc) uses dnf and glibc's SunRPC/XDR + execinfo. - # musllinux (Alpine/musl) has neither: install libtirpc for the system - # XDR path (ALPS_HAVE_RPC_XDR_H) and disable the execinfo backtrace. - CIBW_BEFORE_ALL_LINUX: > - if command -v dnf >/dev/null 2>&1; - then dnf install -y ccache cmake hdf5-devel lapack-devel ninja-build; EXTRA=; - else apk add --no-cache ccache ninja-is-really-ninja hdf5-dev lapack-dev libtirpc-dev; - ln -sf /usr/include/tirpc/rpc /usr/include/rpc; - ln -sf /usr/include/tirpc/netconfig.h /usr/include/netconfig.h; - export CXXFLAGS="-DALPS_NGS_NO_STACKTRACE"; - EXTRA="-DALPS_HAVE_RPC_XDR_H=1 -DCMAKE_CXX_STANDARD_LIBRARIES=-ltirpc"; - fi && - cd {project} && - cmake --preset wheel-deps - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - $EXTRA && - cmake --build --preset wheel-deps -j2 - CIBW_BEFORE_ALL_MACOS: > - brew install ccache cmake hdf5 ninja && - cd {project} && - cmake --preset wheel-deps - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - -DHDF5_ROOT=$(brew --prefix hdf5) && - cmake --build --preset wheel-deps -j2 - CIBW_ENVIRONMENT_MACOS: > - ALPS_DIR=$(pwd)/_build/wheel-deps/install/share/alps - CCACHE_DIR=$(pwd)/_build/ccache - CCACHE_NAMESPACE=pyalps-wheel - CMAKE_ARGS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" - MACOSX_DEPLOYMENT_TARGET=${{ matrix.plat.target }} - CXXFLAGS="-stdlib=libc++" - CIBW_REPAIR_WHEEL_COMMAND_LINUX: > - auditwheel repair -w {dest_dir} {wheel} && - auditwheel show {dest_dir}/*.whl - CIBW_REPAIR_WHEEL_COMMAND_MACOS: > - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && - delocate-listdeps --all {dest_dir}/*.whl - CIBW_TEST_REQUIRES: pytest - CIBW_TEST_COMMAND: pytest -q {project}/test/pyalps + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.plat.target }} - uses: actions/upload-artifact@v7 with: diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index 44f93b850..3526cb1a1 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -39,4 +39,48 @@ ALPS_DIR = { env = "ALPS_DIR" } "../../../lib/xml" = "_vendor/lib/xml" [tool.cibuildwheel] +build = ["cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] manylinux-x86_64-image = "manylinux_2_28" +test-requires = ["pytest"] +test-command = "pytest -q {project}/test/pyalps" + +# The ALPS C++ SDK is built once per platform in before-all via the wheel-deps +# CMake preset; ALPS_DIR points every wheel build at that install. The ccache +# in _build/ccache spans the SDK and all wheel builds (cached across CI runs). +[tool.cibuildwheel.environment] +ALPS_DIR = "$(pwd)/_build/wheel-deps/install/share/alps" +CCACHE_DIR = "$(pwd)/_build/ccache" +CCACHE_NAMESPACE = "pyalps-wheel" +CMAKE_ARGS = "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + +# manylinux (AlmaLinux/glibc) uses dnf and glibc's SunRPC/XDR + execinfo. +# musllinux (Alpine/musl) has neither: install libtirpc for the system +# XDR path (ALPS_HAVE_RPC_XDR_H) and disable the execinfo backtrace. +[tool.cibuildwheel.linux] +before-all = [ + "if command -v dnf >/dev/null 2>&1; then dnf install -y ccache cmake hdf5-devel lapack-devel ninja-build; EXTRA=; else apk add --no-cache ccache ninja-is-really-ninja hdf5-dev lapack-dev libtirpc-dev; ln -sf /usr/include/tirpc/rpc /usr/include/rpc; ln -sf /usr/include/tirpc/netconfig.h /usr/include/netconfig.h; export CXXFLAGS=-DALPS_NGS_NO_STACKTRACE; EXTRA='-DALPS_HAVE_RPC_XDR_H=1 -DCMAKE_CXX_STANDARD_LIBRARIES=-ltirpc'; fi", + "cd {project}", + "cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache $EXTRA", + "cmake --build --preset wheel-deps -j2", +] +repair-wheel-command = [ + "auditwheel repair -w {dest_dir} {wheel}", + "auditwheel show {dest_dir}/*.whl", +] + +[tool.cibuildwheel.macos] +before-all = [ + "brew install ccache cmake hdf5 ninja", + "cd {project}", + "cmake --preset wheel-deps -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DHDF5_ROOT=$(brew --prefix hdf5)", + "cmake --build --preset wheel-deps -j2", +] +repair-wheel-command = [ + "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}", + "delocate-listdeps --all {dest_dir}/*.whl", +] + +[[tool.cibuildwheel.overrides]] +select = "*-macosx_*" +inherit.environment = "append" +environment = { CXXFLAGS = "-stdlib=libc++" } From bb72c65d1816340af6b15a8bef00616cc74ad728 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 13:24:17 -0500 Subject: [PATCH 14/52] ci: smoke test packaged artifacts before upload cibuildwheel already tests each wheel inside its build environment; this adds checks on the artifacts as they would reach PyPI. The sdist job now runs twine check and asserts the vendored ALPS sources are present, and a smoke_test matrix installs the repaired wheel with pip on a clean runner (oldest and newest supported Python per platform) and runs the binding surface tests. upload_pypi is gated on all of it. Co-Authored-By: Claude Fable 5 --- .github/workflows/build_wheels.yml | 52 +++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index a23e5e591..297f2131a 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -59,14 +59,64 @@ jobs: - name: Build sdist run: pipx run build --sdist --outdir dist bindings/python/pyalps + # The sdist must remain buildable outside the repository checkout: + # verify its metadata and that the vendored ALPS sources it needs + # (applications, tools, XML library) actually made it in. + - name: Smoke check sdist + run: | + pipx run twine check dist/*.tar.gz + tar -tzf dist/*.tar.gz > sdist-manifest.txt + for path in _vendor/lib/xml/ALPS.xsl _vendor/tool/maxent.cpp \ + _vendor/applications/dmft/qmc _vendor/applications/qmc/dwa \ + LICENSE.txt src/pyalps/__init__.py; do + grep -q "$path" sdist-manifest.txt || { echo "missing $path in sdist"; exit 1; } + done + - uses: actions/upload-artifact@v7 with: name: cibw-sdist path: dist/*.tar.gz + # cibuildwheel already runs test/pyalps against every wheel inside the build + # environment; this job checks the artifacts as they would reach PyPI: the + # repaired, uploaded-and-downloaded wheel installed with pip on a clean + # runner, at the oldest and newest supported Python. + smoke_test: + name: Smoke test wheels on ${{ matrix.os }} / py${{ matrix.python }} + needs: [build_wheels] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15, macos-26] + python: ["3.10", "3.14"] + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python }} + + - uses: actions/download-artifact@v8 + with: + pattern: cibw-wheels-* + path: wheelhouse + merge-multiple: true + + - name: Install wheel from artifacts + run: | + pipx run twine check wheelhouse/*.whl + python -m pip install numpy scipy pytest + python -m pip install --no-index --no-deps --find-links wheelhouse pyalps + + - name: Import and run binding surface tests + run: | + python -c "import pyalps, pyalps.alea, pyalps.hdf5, pyalps.pytools; print(pyalps.__file__)" + python -m pytest -q test/pyalps + upload_pypi: - needs: [build_wheels, build_sdist] + needs: [build_wheels, build_sdist, smoke_test] runs-on: ubuntu-latest environment: pypi permissions: From 2d3a4806310272aec35375d94a808cb2c0192d2e Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 13:52:17 -0500 Subject: [PATCH 15/52] ci: bump actions/cache to v6 v4 targets Node.js 20, which GitHub runners now warn is deprecated. Co-Authored-By: Claude Fable 5 --- .github/workflows/build_wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 297f2131a..287269f1d 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v7 - name: Restore compiler cache - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: _build/ccache key: pyalps-ccache-${{ runner.os }}-${{ matrix.plat.arch }}-${{ hashFiles('src/**', 'bindings/python/**', 'applications/**', 'tool/maxent*') }} From 31e623da02e6f645658a1ddc32cec99176fba49d Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 15:56:18 -0500 Subject: [PATCH 16/52] fix(pyalps): address nanobind-migration audit findings Restore legacy HDF5 list save semantics (audit issue 13): exact-type homogeneous rectangular list trees are written as one N-D dataset that keeps the element type ([1,2,3] stays int32, floats stay float64, out-of-int32-range widens to int64), equal-shape numpy-array lists stack via numpy, and bool-containing / mixed-type / ragged lists fall back to the legacy per-index group descent. The previous probe ladder cast with implicit conversion enabled and double first, so integer lists were silently written as float64. Unify the three dict->params converters into dict_to_params.hpp (issue 17): params, mcbase and the application modules now ingest values identically; oversized ints raise instead of silently truncating through paramvalue's 32-bit int; int lists round-trip as ints; complex scalars are supported; None is rejected with a message that names it (issue 8). Copy __eq__/__ne__/__hash__ onto the MutableMapping shims (issue 1): the hasattr guard could never copy them (object provides both), so mapping equality was lost relative to the Boost.Python __bases__ inheritance. Move observable.__lshift__ from a type monkeypatch into the C++ binding, returning self for chaining (issue 2). Forward save/load through the mcbase trampoline (issue 9) so Python overrides are reached by C++ virtual dispatch; slot count pinned to the five virtuals in src/alps/mcbase.hpp. Smaller items: cache the numpy module and use limited-API PyTuple_SetItem in numpy_compat.hpp (issues 24, 29); release the previous entry on archive-exception re-registration (issue 12); in-place accumulator result operators use rv_policy::none + is_operator (issue 11); document copy semantics on the dwa worldlines accessors (issue 7) and overload ordering in pyalea (issue 14); mirror libalps' BOOST_* config defines in the bindings build (issue 16); replace the dead pyalps.mpi import chain with a clear ImportError (issue 18); cap nanobind below the next major (issue 19). Issues 10/43 (duplicate wrapper) and the issue-11 leak were checked empirically against nanobind 2.15 and refuted; comments record the verified behaviour. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/CMakeLists.txt | 8 + bindings/python/pyalps/cpp/dict_to_params.hpp | 83 ++++++-- .../python/pyalps/cpp/ngs/accumulator.cpp | 20 +- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 181 ++++++++++++++++-- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 58 ++---- bindings/python/pyalps/cpp/ngs/observable.cpp | 13 ++ .../python/pyalps/cpp/ngs/observables.cpp | 4 + bindings/python/pyalps/cpp/ngs/params.cpp | 69 +------ bindings/python/pyalps/cpp/numpy_compat.hpp | 21 +- bindings/python/pyalps/cpp/pyalea.cpp | 8 + bindings/python/pyalps/pyproject.toml | 5 +- bindings/python/pyalps/src/pyalps/mpi.py | 39 ++-- bindings/python/pyalps/src/pyalps/ngs.py | 15 +- test/pyalps/hlist_test.output | 9 - test/pyalps/mcdata.output | 180 ----------------- test/pyalps/pyhdf5io.output | 39 ---- test/pyalps/pyparams.output | 15 -- test/pyalps/run_python_test.cmake | 66 ------- 18 files changed, 346 insertions(+), 487 deletions(-) delete mode 100644 test/pyalps/hlist_test.output delete mode 100644 test/pyalps/mcdata.output delete mode 100644 test/pyalps/pyhdf5io.output delete mode 100644 test/pyalps/pyparams.output delete mode 100644 test/pyalps/run_python_test.cmake diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index 55d9bdb24..f006cb0b3 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -124,6 +124,14 @@ if(PYALPS_BUILD_APPLICATIONS) endif() foreach(_target IN LISTS _pyalps_targets) + # Mirror the Boost configuration macros libalps is compiled with + # (root CMakeLists.txt, CMAKE_CXX_FLAGS) so the Boost headers both + # sides of the ALPS library boundary include are configured + # identically. + target_compile_definitions(${_target} PRIVATE + BOOST_NO_AUTO_PTR + BOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF + BOOST_TIMER_ENABLE_DEPRECATED) target_include_directories(${_target} PRIVATE ${ALPS_INCLUDE_DIRS} ${ALPS_EXTRA_INCLUDE_DIRS}) diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index d50c84e8d..b3b7bc20e 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -1,34 +1,85 @@ // Copyright (C) 2026 by the ALPS collaboration // Part of the ALPS Project — see LICENSE.txt for full license text. // SPDX-License-Identifier: MIT +// +// The single Python→alps::params conversion ladder, shared by +// pyngsparams_c (__setitem__ / dict ctor), pyngsbase_c (mcbase ctor) +// and the application modules (maxent_c / cthyb / ctint), so every +// module ingests parameters identically. #ifndef PYALPS_DICT_TO_PARAMS_HPP #define PYALPS_DICT_TO_PARAMS_HPP #include #include +#include #include #include +#include #include #include namespace pyalps { namespace nb = nanobind; +// Store one Python value under `key`. paramvalue's only integral +// alternative is a 32-bit int and libalps static_casts wider integer +// types down to it, so out-of-range Python ints are rejected loudly +// here rather than truncated silently. List probes use exact element +// types first (convert=false) so integer lists round-trip as ints; +// mixed numeric lists without bools widen to double. +inline void set_param_value(alps::params & p, std::string const & key, nb::handle value) { + if (value.is_none()) + throw nb::type_error(("cannot store None for parameter '" + key + + "': params has no null type; delete the key instead").c_str()); + if (nb::isinstance(value)) { + p[key] = nb::cast(value); + } else if (nb::isinstance(value)) { + try { + p[key] = nb::cast(value); + } catch (nb::cast_error const &) { + throw nb::type_error(("parameter '" + key + + "' does not fit params' 32-bit integer type").c_str()); + } + } else if (nb::isinstance(value)) { + p[key] = nb::cast(value); + } else if (PyComplex_Check(value.ptr())) { + p[key] = nb::cast>(value); + } else if (nb::isinstance(value)) { + p[key] = nb::cast(value); + } else if (nb::isinstance(value) || nb::isinstance(value)) { + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + // mixed numeric content (e.g. [1, 2.5]) widens to double — + // but never bools, which would silently become 0.0/1.0 + nb::object seq = nb::borrow(value); + std::size_t const n = nb::len(seq); + bool has_bool = false; + for (std::size_t i = 0; i < n && !has_bool; ++i) { + nb::object item = seq[i]; + has_bool = nb::isinstance(item); + } + if (!has_bool) { + try { p[key] = nb::cast>(value); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>>(value); return; } + catch (nb::cast_error const &) {} + } + throw nb::type_error(("unsupported list for parameter '" + key + + "' (expected homogeneous numbers or strings)").c_str()); + } else { + throw nb::type_error(("unsupported type for parameter '" + key + + "' (expected bool/int/float/complex/str or a list of those)").c_str()); + } +} inline alps::params params_from_dict(nb::dict const & values) { alps::params result; - for (auto item : values) { - std::string key = nb::cast(nb::str(item.first)); - nb::handle value = item.second; - if (nb::isinstance(value)) - result[key] = nb::cast(value); - else if (nb::isinstance(value)) - result[key] = nb::cast(value); - else if (nb::isinstance(value)) - result[key] = nb::cast(value); - else if (nb::isinstance(value)) - result[key] = nb::cast(value); - else if (nb::isinstance(value) || nb::isinstance(value)) - result[key] = nb::cast>(value); - else - throw nb::type_error(("unsupported parameter type for '" + key + "'").c_str()); - } + for (auto item : values) + set_param_value(result, + nb::cast(nb::str(item.first)), + item.second); return result; } } // namespace pyalps diff --git a/bindings/python/pyalps/cpp/ngs/accumulator.cpp b/bindings/python/pyalps/cpp/ngs/accumulator.cpp index f3ca7f005..9cb2824bf 100644 --- a/bindings/python/pyalps/cpp/ngs/accumulator.cpp +++ b/bindings/python/pyalps/cpp/ngs/accumulator.cpp @@ -28,14 +28,18 @@ template void bind_result_operators(nb::class_ & cls) { cls .def("__neg__", [](Result value) { value.negate(); return value; }) - .def("__iadd__", [](Result & self, Result const & other) -> Result & { self += other; return self; }, nb::rv_policy::reference_internal) - .def("__iadd__", [](Result & self, double value) -> Result & { self += value; return self; }, nb::rv_policy::reference_internal) - .def("__isub__", [](Result & self, Result const & other) -> Result & { self -= other; return self; }, nb::rv_policy::reference_internal) - .def("__isub__", [](Result & self, double value) -> Result & { self -= value; return self; }, nb::rv_policy::reference_internal) - .def("__imul__", [](Result & self, Result const & other) -> Result & { self *= other; return self; }, nb::rv_policy::reference_internal) - .def("__imul__", [](Result & self, double value) -> Result & { self *= value; return self; }, nb::rv_policy::reference_internal) - .def("__itruediv__", [](Result & self, Result const & other) -> Result & { self /= other; return self; }, nb::rv_policy::reference_internal) - .def("__itruediv__", [](Result & self, double value) -> Result & { self /= value; return self; }, nb::rv_policy::reference_internal) + // In-place operators return *this: rv_policy::none hands back + // the existing Python wrapper without any ownership or + // keep_alive bookkeeping, and is_operator() gives the standard + // NotImplemented behaviour on foreign operand types. + .def("__iadd__", [](Result & self, Result const & other) -> Result & { self += other; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__iadd__", [](Result & self, double value) -> Result & { self += value; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__isub__", [](Result & self, Result const & other) -> Result & { self -= other; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__isub__", [](Result & self, double value) -> Result & { self -= value; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__imul__", [](Result & self, Result const & other) -> Result & { self *= other; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__imul__", [](Result & self, double value) -> Result & { self *= value; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__itruediv__", [](Result & self, Result const & other) -> Result & { self /= other; return self; }, nb::rv_policy::none, nb::is_operator()) + .def("__itruediv__", [](Result & self, double value) -> Result & { self /= value; return self; }, nb::rv_policy::none, nb::is_operator()) .def("__add__", [](Result value, Result const & other) { value += other; return value; }, nb::is_operator()) .def("__add__", [](Result value, double other) { value += other; return value; }, nb::is_operator()) .def("__radd__", [](Result value, double other) { value += other; return value; }, nb::is_operator()) diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 05eeefe92..128e7de23 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -27,12 +27,95 @@ #include #include #include +#include #include #include #include namespace nb = nanobind; namespace alps { namespace detail { + // Analysis of a Python list/tuple tree against the legacy + // Boost.Python vectorization rules (src/alps/hdf5/python.cpp, + // is_vectorizable_generic): a list is written as one dataset + // only when every leaf has the SAME exact scalar type — plain + // bool was never a vectorizable dtype — and all nested extents + // are rectangular. Everything else becomes a group with one + // child per index. The checked-in pyhdf5io fixture documents + // this contract ([1, 2, 3] must stay int32 on disk). + struct list_vectorizer { + enum class leaf_kind { none, integral, floating, cplx, text }; + std::vector extent; // rectangular extents per depth + std::ptrdiff_t leaf_depth = -1; + leaf_kind kind = leaf_kind::none; + std::vector ints; + std::vector reals; + std::vector> cplxs; + std::vector texts; + bool fits_int = true; + bool analyze(nb::handle node, std::size_t depth) { + nb::object seq = nb::borrow(node); + std::size_t const n = nb::len(seq); + if (depth == extent.size()) + extent.push_back(n); + else if (extent[depth] != n) + return false; // ragged + for (std::size_t i = 0; i < n; ++i) { + nb::object item = seq[i]; + PyObject * p = item.ptr(); + if (PyBool_Check(p)) + return false; // legacy: bool never vectorizes + if (PyList_Check(p) || PyTuple_Check(p)) { + // a sequence may not appear at the leaf level + if (leaf_depth != -1 + && static_cast(depth + 1) >= leaf_depth) + return false; + if (!analyze(item, depth + 1)) + return false; + continue; + } + // scalar leaf: all leaves must sit at one depth + if (leaf_depth == -1) { + if (extent.size() != depth + 1) + return false; + leaf_depth = static_cast(depth + 1); + } else if (leaf_depth != static_cast(depth + 1)) + return false; + if (PyLong_Check(p)) { + int overflow = 0; + long long v = PyLong_AsLongLongAndOverflow(p, &overflow); + if (overflow) + return false; // → descent; the per-element save raises, like legacy + if (!accept(leaf_kind::integral)) + return false; + if (v < std::numeric_limits::min() || v > std::numeric_limits::max()) + fits_int = false; + ints.push_back(v); + } else if (PyFloat_Check(p)) { + // includes numpy.float64, which subclasses float + if (!accept(leaf_kind::floating)) + return false; + reals.push_back(PyFloat_AsDouble(p)); + } else if (PyComplex_Check(p)) { + // includes numpy.complex128, which subclasses complex + if (!accept(leaf_kind::cplx)) + return false; + Py_complex c = PyComplex_AsCComplex(p); + cplxs.emplace_back(c.real, c.imag); + } else if (PyUnicode_Check(p)) { + if (!accept(leaf_kind::text)) + return false; + texts.push_back(nb::cast(item)); + } else + return false; // numpy scalars/arrays, other objects + } + return true; + } + bool accept(leaf_kind k) { + if (kind == leaf_kind::none) + kind = k; + return kind == k; // legacy: mixed scalar kinds → group + } + }; // Save-side visitor: receives a concrete C++ value (or a // nb::list / nb::dict) from extract_from_pyobject_py11 and // writes it to the archive at `path`. @@ -51,25 +134,71 @@ namespace alps { ar << alps::make_pvp(path, ptr, sizes); } void operator()(nb::list const & l) const { - // Order: flat numeric first, then nested numeric, then - // strings. Heterogeneous / deeply-nested / mixed-type - // lists fall through to the descent branch below which - // stores each entry under a numeric child path. - try { ar[path] << nb::cast>(l); return; } - catch (nb::cast_error const &) {} - try { ar[path] << nb::cast>(l); return; } - catch (nb::cast_error const &) {} - try { ar[path] << nb::cast>>(l); return; } - catch (nb::cast_error const &) {} - try { ar[path] << nb::cast>>(l); return; } - catch (nb::cast_error const &) {} - try { ar[path] << nb::cast>>(l); return; } - catch (nb::cast_error const &) {} - try { ar[path] << nb::cast>(l); return; } - catch (nb::cast_error const &) {} - // Inhomogeneous — recurse per-element into - // /, letting each entry be stored as its - // own native type. + // Reproduce the legacy vectorization rules (see + // list_vectorizer above): exact-type homogeneous + // rectangular list trees become one N-D dataset that + // keeps the element type — [1, 2, 3] stays int32 on + // disk, floats stay float64 — while bool-containing, + // mixed-type and ragged lists become a group with one + // child per index. + if (nb::len(l) == 0) { + // legacy wrote an empty integer dataset + ar[path] << std::vector(); + return; + } + list_vectorizer v; + if (v.analyze(l, 0)) { + switch (v.kind) { + case list_vectorizer::leaf_kind::integral: + if (v.fits_int) { + std::vector buf(v.ints.begin(), v.ints.end()); + (*this)(buf.data(), v.extent); + } else + (*this)(v.ints.data(), v.extent); + return; + case list_vectorizer::leaf_kind::floating: + (*this)(v.reals.data(), v.extent); + return; + case list_vectorizer::leaf_kind::cplx: + (*this)(v.cplxs.data(), v.extent); + return; + case list_vectorizer::leaf_kind::text: + if (v.extent.size() == 1) { + ar[path] << v.texts; + return; + } + break; // nested string lists → group descent + case list_vectorizer::leaf_kind::none: + break; // e.g. [[], []] → group descent + } + } else if (all_ndarrays(l)) { + // Legacy stacked equal-shape numpy arrays into one + // dataset; delegate to numpy so shape checking and + // dtype promotion match numpy's rules, then feed + // the stacked array through the ndarray save path. + // Ragged shapes (numpy raises) and object dtype + // fall through to the group descent below. + nb::object arr; + try { + arr = nb::borrow(alps::python::numpy_module()) + .attr("asarray")(l); + } catch (nb::python_error &) { + arr = nb::object(); + } + if (arr.is_valid()) { + std::string dtype_kind = + nb::cast(arr.attr("dtype").attr("kind")); + if (dtype_kind.find_first_of("biufc") != std::string::npos + && dtype_kind.size() == 1) { + hdf5_save_py11_visitor child_visitor{ar, path}; + extract_from_pyobject_py11(child_visitor, arr); + return; + } + } + } + // Heterogeneous / ragged / bool-containing — recurse + // per-element into /, letting each entry + // be stored as its own native type (legacy behaviour). ar.create_group(path); Py_ssize_t i = 0; for (auto item : l) { @@ -78,6 +207,12 @@ namespace alps { extract_from_pyobject_py11(child_visitor, item); } } + static bool all_ndarrays(nb::list const & l) { + for (auto item : l) + if (std::string(item.ptr()->ob_type->tp_name) != "numpy.ndarray") + return false; + return true; + } void operator()(nb::dict const & d) const { // Store a dict as a group with one child per key. Keys // are stringified (HDF5 paths are strings), values go @@ -252,10 +387,14 @@ namespace alps { if (id < 0 || id >= static_cast(exception_type.size())) throw std::out_of_range( "register_archive_exception_type: id out of range"); - // Py_INCREF the incoming type so it survives past this call - // (we're keeping a raw PyObject* in a static array). + // Keep a strong reference in the static table — the entry + // is deliberately pinned until process exit because the + // translators can fire at any time — but release any + // previous entry so re-registration doesn't leak it. + PyObject * previous = exception_type[id]; Py_INCREF(type.ptr()); exception_type[id] = type.ptr(); + Py_XDECREF(previous); } } } diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index 0c4b704c7..21bc6bff1 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -38,9 +38,10 @@ // libalps still declares a params(boost::python::dict) ctor in its // header, but we don't want to drag boost::python through the // nanobind bindings. Instead, we convert nb::dict → alps::params at the -// binding boundary by iterating and setitem-ing concrete C++ values -// (int/float/bool/str/list). That sidesteps the cross-registry issue -// and keeps the libalps ABI untouched. +// binding boundary through the shared ladder in ../dict_to_params.hpp, +// so mcbase, params and the application modules ingest parameters +// identically. That sidesteps the cross-registry issue and keeps the +// libalps ABI untouched. #define PY_ARRAY_UNIQUE_SYMBOL pyngsbase_PyArrayHandle #include #include @@ -59,37 +60,7 @@ namespace nb = nanobind; #include #include #include -namespace alps { - namespace detail { - // Convert a Python dict into an alps::params, extracting concrete - // C++ values for each entry. This mirrors what the libalps - // params(boost::python::dict) ctor does, but without routing the - // nb::object through the boost::python::object variant alternative - // — everything stays within the nanobind type registry. - inline alps::params py_dict_to_params(nb::dict const & d) { - alps::params p; - for (auto item : d) { - std::string k = nb::cast(nb::str(item.first)); - nb::handle v = item.second; - if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v) || nb::isinstance(v)) - p[k] = nb::cast>(v); - else - throw nb::type_error(( - "unsupported type for key '" + k + - "' in params dict (expected bool/int/float/str/list)").c_str()); - } - return p; - } - } -} +#include "../dict_to_params.hpp" namespace alps { // Trampoline: holds Python overrides for pure-virtuals. The // protected mcbase members (random / parameters / measurements) @@ -98,16 +69,21 @@ namespace alps { // class's own member functions / friends). class PyMCBase : public mcbase { public: - NB_TRAMPOLINE(mcbase, 3); + // Slot count = the number of NB_OVERRIDE* calls below. + // mcbase (src/alps/mcbase.hpp) declares five virtuals: + // update / measure / fraction_completed (pure) and + // save(archive&) / load(archive&); all five must be + // forwarded so Python overrides are seen by C++ callers. + NB_TRAMPOLINE(mcbase, 5); #ifdef ALPS_HAVE_MPI PyMCBase(nb::dict const & arg, std::size_t seed_offset = 42, boost::mpi::communicator const & /*comm*/ = boost::mpi::communicator()) - : mcbase(alps::detail::py_dict_to_params(arg), seed_offset) + : mcbase(pyalps::params_from_dict(arg), seed_offset) {} #else PyMCBase(nb::dict const & arg, std::size_t seed_offset = 42) - : mcbase(alps::detail::py_dict_to_params(arg), seed_offset) + : mcbase(pyalps::params_from_dict(arg), seed_offset) {} #endif void update() override { @@ -119,6 +95,14 @@ namespace alps { double fraction_completed() const override { NB_OVERRIDE_PURE(fraction_completed); } + // Non-pure: fall through to the C++ implementation when the + // Python subclass doesn't override (NB_OVERRIDE, not _PURE). + void save(alps::hdf5::archive & ar) const override { + NB_OVERRIDE(save, ar); + } + void load(alps::hdf5::archive & ar) override { + NB_OVERRIDE(load, ar); + } // Accessors for protected mcbase members. Called from the // binding lambdas below (they friend-in through PyMCBase). alps::random01 & get_random() { return random; } diff --git a/bindings/python/pyalps/cpp/ngs/observable.cpp b/bindings/python/pyalps/cpp/ngs/observable.cpp index dde673f21..8c653517b 100644 --- a/bindings/python/pyalps/cpp/ngs/observable.cpp +++ b/bindings/python/pyalps/cpp/ngs/observable.cpp @@ -74,8 +74,21 @@ NB_MODULE(pyngsobservable_c, m) { m.def("createRealVectorObservable", &alps::detail::create_RealVectorObservable_export); nb::class_(m, "observable") .def("append", &alps::detail::observable_append) + // obs << value appends and returns obs so it chains. Bound in + // C++ (rv_policy::none returns the existing wrapper) instead of + // the former ngs.py monkeypatch onto the extension type, which + // would break if nanobind ever marks its types immutable. + .def("__lshift__", + [](alps::mcobservable & self, nb::object const & data) -> alps::mcobservable & { + alps::detail::observable_append(self, data); + return self; + }, + nb::rv_policy::none) .def("merge", &alps::mcobservable::merge) .def("save", &alps::mcobservable::save) .def("load", &alps::detail::observable_load) + // Mirrors the legacy Boost.Python module, which (oddly, but + // load-compatibly) bound addToObservable to the same helper + // as load. .def("addToObservable", &alps::detail::observable_load); } diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index 7b121fe64..1813112b6 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -62,6 +62,10 @@ void createRealVectorObservable(alps::mcobservables & self, std::string const & void addObservable(alps::mcobservables & self, nb::object const & obj) { // Mirror boost::python::call_method(obj, "addToObservables", ref(self)): // bounce the call back into Python, passing `self` by reference. + // nanobind's instance registry returns the already-registered + // wrapper for &self (the one this call came through, verified + // empirically), so the callback sees the identical Python object — + // no duplicate wrapper, no separate lifetime to manage. obj.attr("addToObservables")(nb::cast(&self, nb::rv_policy::reference)); } } // namespace diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index 0994346f9..93238e513 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: MIT #include #include +#include #include #include #include @@ -18,13 +19,9 @@ #include #include #include +#include "../dict_to_params.hpp" namespace nb = nanobind; namespace { -// Convert a Python dict into an alps::params. Same shape as the -// helper in mcbase.cpp but kept local to params.cpp so a change to -// the dispatch (e.g. adding complex support) can stay in one place -// alongside the other setitem logic. -alps::params py_dict_to_params(nb::dict const & d); // Walk the paramvalue variant and wrap each native alternative as a // nb::object. Called from __getitem__. struct paramvalue_to_py_visitor : boost::static_visitor { @@ -39,39 +36,21 @@ nb::object paramvalue_to_py(alps::detail::paramvalue const & pv) { static_cast(pv)); } // Deposit a native C++ value from a Python object into the paramvalue -// via paramproxy's templated operator=. +// via paramproxy's templated operator= — shared ladder in +// ../dict_to_params.hpp so params, mcbase and the application modules +// all ingest values identically. void params_setitem(alps::params & self, nb::object const & key_obj, nb::object const & value) { - std::string key = nb::cast(nb::str(key_obj)); - if (nb::isinstance(value)) - self[key] = nb::cast(value); - else if (nb::isinstance(value)) - self[key] = nb::cast(value); - else if (nb::isinstance(value)) - self[key] = nb::cast(value); - else if (nb::isinstance(value)) - self[key] = nb::cast(value); - else if (nb::isinstance(value) || nb::isinstance(value)) { - // Heuristic: try doubles first, strings as fallback. - try { - self[key] = nb::cast>(value); - } catch (nb::cast_error &) { - self[key] = nb::cast>(value); - } - } else { - throw nb::type_error("unsupported value type for params[]"); - } + pyalps::set_param_value(self, nb::cast(nb::str(key_obj)), value); } nb::object params_getitem(alps::params & self, nb::object const & key_obj) { std::string key = nb::cast(nb::str(key_obj)); - if (!self.defined(key)) - return nb::none(); // params doesn't expose the underlying map directly, but - // paramiterator yields (key, paramvalue) pairs; walk it to find the - // entry and hand the variant to paramvalue_to_py. + // paramiterator yields (key, paramvalue) pairs; a single walk both + // answers "defined?" and hands the variant to paramvalue_to_py. for (auto it = self.begin(); it != self.end(); ++it) if (it->first == key) return paramvalue_to_py(it->second); - return nb::none(); // defensive — defined()==true should guarantee a hit + return nb::none(); } void params_delitem(alps::params & self, nb::object const & key_obj) { self.erase(nb::cast(nb::str(key_obj))); @@ -97,41 +76,13 @@ std::string params_print(alps::params & self) { alps::params params_deepcopy(alps::params const & self, nb::handle /*memo*/) { return alps::params(self); } -// Materialise an alps::params from a Python dict. Re-uses the same -// type dispatch as params_setitem so a round-tripped dict-built -// params contains exactly the same variant alternatives. -alps::params py_dict_to_params(nb::dict const & d) { - alps::params p; - for (auto item : d) { - std::string k = nb::cast(nb::str(item.first)); - nb::handle v = item.second; - if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v)) - p[k] = nb::cast(v); - else if (nb::isinstance(v) || nb::isinstance(v)) { - try { p[k] = nb::cast>(v); } - catch (nb::cast_error &) { - p[k] = nb::cast>(v); - } - } else { - throw nb::type_error( - ("unsupported value type for params key '" + k + "'").c_str()); - } - } - return p; -} } // namespace NB_MODULE(pyngsparams_c, m) { nb::class_(m, "params") .def(nb::init<>()) .def("__init__", [](alps::params * self, nb::dict const & d) { - new (self) alps::params(py_dict_to_params(d)); + new (self) alps::params(pyalps::params_from_dict(d)); }, nb::arg("dict")) // Read a classic ALPS text parameter file, matching the str diff --git a/bindings/python/pyalps/cpp/numpy_compat.hpp b/bindings/python/pyalps/cpp/numpy_compat.hpp index b23cafc0e..187b86781 100644 --- a/bindings/python/pyalps/cpp/numpy_compat.hpp +++ b/bindings/python/pyalps/cpp/numpy_compat.hpp @@ -38,17 +38,30 @@ namespace alps { template <> struct numpy_dtype { static constexpr char const* name = "float64"; }; template <> struct numpy_dtype> { static constexpr char const* name = "complex64"; }; template <> struct numpy_dtype> { static constexpr char const* name = "complex128"; }; + // Cached numpy module. Importing per call was a sys.modules + // lookup + import-lock acquisition on every array conversion. + // The reference is deliberately leaked: a static nb_::object + // would decref during static destruction, potentially after + // interpreter finalization. + inline nb_::handle numpy_module() { + static PyObject * mod = nb_::module_::import_("numpy").release().ptr(); + return mod; + } // Allocates numpy.empty(shape, dtype=numpy_dtype::name) and // memcpy's `data` (length = product(shape)) into it. Returns // a writable numpy.ndarray. template inline nb_::object make_numpy_array(T const* data, std::vector const& shape) { - nb_::object np = nb_::module_::import_("numpy"); + nb_::handle np = numpy_module(); nb_::tuple shape_tuple = nb_::steal(PyTuple_New(static_cast(shape.size()))); + // PyTuple_SetItem (not the SET_ITEM macro): the macro pokes + // tuple internals directly and is unavailable under the + // limited API, which is otherwise within reach for these + // bindings. for (std::size_t i = 0; i < shape.size(); ++i) - PyTuple_SET_ITEM(shape_tuple.ptr(), static_cast(i), - PyLong_FromUnsignedLongLong(shape[i])); + PyTuple_SetItem(shape_tuple.ptr(), static_cast(i), + PyLong_FromUnsignedLongLong(shape[i])); nb_::object arr = np.attr("empty")( shape_tuple, nb_::arg("dtype") = numpy_dtype::name); // Bridge the freshly-allocated numpy buffer through nb::ndarray @@ -84,7 +97,7 @@ namespace alps { // of through the numpy C headers at compile time. template inline contiguous_view as_contiguous(nb_::handle obj) { - nb_::object np = nb_::module_::import_("numpy"); + nb_::handle np = numpy_module(); nb_::object arr = np.attr("ascontiguousarray")( obj, nb_::arg("dtype") = numpy_dtype::name); auto nd = nb_::cast>(arr); diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp index fb9b2b86c..34756f9f8 100644 --- a/bindings/python/pyalps/cpp/pyalea.cpp +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -345,6 +345,14 @@ NB_MODULE(pyalea_c, m) { m.def("variance", &variance_vector>>); // integrated_autocorrelation_time — scalar only. The C++ signature // takes the (slope, intercept) pair by const-ref. + // + // NOTE: four overloads in total — the two std::pair forms here + // (satisfied by any 2-tuple via ) and the two + // StdPairDouble forms further down. They are disjoint today because + // StdPairDouble's implicit conversion to std::pair is invisible to + // nanobind; keep the pair overloads registered FIRST and do not add + // an implicitly_convertible between the two, or the dispatch order + // silently changes. m.def("integrated_autocorrelation_time", static_cast const &, std::pair const &)>( diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index 3526cb1a1..06a42f388 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -1,5 +1,8 @@ [build-system] -requires = ["scikit-build-core>=1.0", "nanobind>=2.10"] +# nanobind is capped below the next major: all extension modules in one +# process must agree on the nanobind ABI, and its API/ABI may break at +# major versions. Bump the cap deliberately, with a full test run. +requires = ["scikit-build-core>=1.0", "nanobind>=2.10,<3"] build-backend = "scikit_build_core.build" [project] diff --git a/bindings/python/pyalps/src/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py index bd984bc3b..72d4ed87b 100644 --- a/bindings/python/pyalps/src/pyalps/mpi.py +++ b/bindings/python/pyalps/src/pyalps/mpi.py @@ -1,34 +1,23 @@ # **************************************************************************** -# +# # ALPS Project: Algorithms and Libraries for Physics Simulations -# +# # ALPS Libraries -# +# # Copyright (C) 2012 by Matthias Troyer # # ALPS Project: https://alps.comp-phys.org/ # SPDX-License-Identifier: MIT -# +# # **************************************************************************** -import sys -if sys.platform == 'linux2': - import DLFCN as dl - flags = sys.getdlopenflags() - sys.setdlopenflags(dl.RTLD_NOW|dl.RTLD_GLOBAL) - try: - try: - from .cxx.mpi_c import * - except ImportError: - from mpi_c import * - except ImportError: - from boost.mpi import * - sys.setdlopenflags(flags) -else: - try: - try: - from .cxx.mpi_c import * - except ImportError: - from mpi_c import * - except ImportError: - from boost.mpi import * \ No newline at end of file +# The Boost.Python-era mpi_c extension is not part of the nanobind +# wheel build (no target builds it), so the old fallback chain +# (.cxx.mpi_c → mpi_c → boost.mpi) could never succeed anyway. Fail +# with an explanation instead of a misleading "No module named +# 'boost'". +raise ImportError( + "pyalps.mpi is not available: the MPI bindings were not ported to the " + "nanobind build of pyalps. Drive MPI-parallel simulations from C++, or " + "use mpi4py for Python-side MPI communication." +) diff --git a/bindings/python/pyalps/src/pyalps/ngs.py b/bindings/python/pyalps/src/pyalps/ngs.py index 9612a0149..516527cf5 100644 --- a/bindings/python/pyalps/src/pyalps/ngs.py +++ b/bindings/python/pyalps/src/pyalps/ngs.py @@ -16,10 +16,6 @@ from .cxx.pyngsparams_c import params from .cxx.pyngsobservable_c import observable -def _observable_lshift(self, other): - self.append(other) - return self -observable.__lshift__ = _observable_lshift class RealObservable: def __init__(self, name, binnum = 0): @@ -48,12 +44,17 @@ def addToObservables(self, observables): #rename this with new ALEA # Boost.Python allowed mutating extension-type base classes after creation. # nanobind extension types use a different allocator/deallocator layout, so # register them as virtual MutableMapping implementations and copy the mixin -# methods onto the concrete classes instead. +# methods onto the concrete classes instead. A method is copied when the +# class doesn't provide its own — "inherited from object" counts as absent, +# otherwise __eq__/__ne__ (which every type inherits from object) would be +# skipped and mapping equality lost. __hash__ rides along as None, exactly +# as MutableMapping inheritance made these types unhashable before. for _mapping_type in (params, observables, results): MutableMapping.register(_mapping_type) for _method in ("keys", "values", "items", "get", "pop", "popitem", - "clear", "update", "setdefault", "__eq__", "__ne__"): - if not hasattr(_mapping_type, _method): + "clear", "update", "setdefault", "__eq__", "__ne__", + "__hash__"): + if getattr(_mapping_type, _method, None) is getattr(object, _method, None): setattr(_mapping_type, _method, getattr(MutableMapping, _method)) from .cxx.pyngsbase_c import mcbase diff --git a/test/pyalps/hlist_test.output b/test/pyalps/hlist_test.output deleted file mode 100644 index 343f1e659..000000000 --- a/test/pyalps/hlist_test.output +++ /dev/null @@ -1,9 +0,0 @@ -[1, 2, 3, 4, 5] -1 -[1, 2] -1 -5 -27 -27 -13 -13 diff --git a/test/pyalps/mcdata.output b/test/pyalps/mcdata.output deleted file mode 100644 index 61ca7b3d8..000000000 --- a/test/pyalps/mcdata.output +++ /dev/null @@ -1,180 +0,0 @@ - -Testing MCScalarData - ------------------------- - -Initialization: - -a: 0.81 +/- 0.1 -b: 1.21 +/- 0.15 -c: -1.5 +/- 0.2 - - -Operation: - -a += b: 2.020000000000 +/- 0.180277563773 -a -= b: -0.010000000000 +/- 0.180277563773 -a *= b: 1.452000000000 +/- 0.216889372723 -a /= b: 0.991735537190 +/- 0.148138359895 - - -a += 2.: 3.200000000000 +/- 0.100000000000 -a -= 2.: -0.800000000000 +/- 0.100000000000 -a *= 2.: 2.400000000000 +/- 0.200000000000 -a /= 2.: 0.600000000000 +/- 0.050000000000 - - -a + b: 0.991735537190 +/- 0.148138359895 -a + 2.: 0.600000000000 +/- 0.050000000000 -2. + a: 1.666666666667 +/- 0.138888888889 -a - b: 0.991735537190 +/- 0.148138359895 -a - 2.: 0.600000000000 +/- 0.050000000000 -2. - a: 1.666666666667 +/- 0.138888888889 -a * b: 0.991735537190 +/- 0.148138359895 -a * 2.: 0.600000000000 +/- 0.050000000000 -2. * a: 1.666666666667 +/- 0.138888888889 -a / b: 0.991735537190 +/- 0.148138359895 -a / 2.: 0.600000000000 +/- 0.050000000000 -2. / a: 1.666666666667 +/- 0.138888888889 - - --a: 1.200000000000 +/- 0.100000000000 -abs(c): 1.500000000000 +/- 0.200000000000 - - -pow(a,2.71): 1.639008390308 +/- 0.370142728145 -a.sq() 1.440000000000 +/- 0.240000000000 -a.sqrt() 1.095445115010 +/- 0.045643546459 -a.cb() 1.728000000000 +/- 0.432000000000 -a.cbrt() 1.062658569183 +/- 0.029518293588 -a.exp() 3.320116922737 +/- 0.332011692274 -a.log() 0.182321556794 +/- 0.083333333333 -a.sin() 0.932039085967 +/- 0.036235775448 -a.cos() 0.362357754477 +/- 0.093203908597 -a.tan() 2.572151622126 +/- 0.761596396721 -a.tanh() 0.833654607012 +/- 0.030501999621 - - - -Testing MCVectorData - ------------------------- - -Manipulation - -X: -2.300000000000 +/- 0.010000000000 -1.200000000000 +/- 0.010000000000 -0.700000000000 +/- 0.010000000000 -Y: -3.300000000000 +/- 0.010000000000 -2.200000000000 +/- 0.010000000000 -1.700000000000 +/- 0.010000000000 -X + Y: -5.600000000000 +/- 0.014142135624 -3.400000000000 +/- 0.014142135624 -2.400000000000 +/- 0.014142135624 -X + 2.: -4.300000000000 +/- 0.010000000000 -3.200000000000 +/- 0.010000000000 -2.700000000000 +/- 0.010000000000 -2. + X: -4.300000000000 +/- 0.010000000000 -3.200000000000 +/- 0.010000000000 -2.700000000000 +/- 0.010000000000 -X + Y: -5.600000000000 +/- 0.014142135624 -3.400000000000 +/- 0.014142135624 -2.400000000000 +/- 0.014142135624 -X + 2.: -4.300000000000 +/- 0.010000000000 -3.200000000000 +/- 0.010000000000 -2.700000000000 +/- 0.010000000000 -2. + X: -4.300000000000 +/- 0.010000000000 -3.200000000000 +/- 0.010000000000 -2.700000000000 +/- 0.010000000000 -X / Y: -0.696969696970 +/- 0.003693697954 -0.545454545455 +/- 0.005177671110 -0.411764705882 +/- 0.006361514294 -X / 2.: -1.150000000000 +/- 0.005000000000 -0.600000000000 +/- 0.005000000000 -0.350000000000 +/- 0.005000000000 -2. / X: -0.869565217391 +/- 0.003780718336 -1.666666666667 +/- 0.013888888889 -2.857142857143 +/- 0.040816326531 -X / Y: -0.696969696970 +/- 0.003693697954 -0.545454545455 +/- 0.005177671110 -0.411764705882 +/- 0.006361514294 -X / 2.: -1.150000000000 +/- 0.005000000000 -0.600000000000 +/- 0.005000000000 -0.350000000000 +/- 0.005000000000 -2. / X: -0.869565217391 +/- 0.003780718336 -1.666666666667 +/- 0.013888888889 -2.857142857143 +/- 0.040816326531 --X: -2.300000000000 +/- 0.010000000000 -1.200000000000 +/- 0.010000000000 -0.700000000000 +/- 0.010000000000 -abs(X): -2.300000000000 +/- 0.010000000000 -1.200000000000 +/- 0.010000000000 -0.700000000000 +/- 0.010000000000 -pow(X,2.71): -9.556138502711 +/- 0.112596240619 -1.639008390308 +/- 0.037014272814 -0.380378260851 +/- 0.014726072670 -X.sq(): -5.290000000000 +/- 0.046000000000 -1.440000000000 +/- 0.024000000000 -0.490000000000 +/- 0.014000000000 -X.sqrt(): -1.516575088810 +/- 0.003296902367 -1.095445115010 +/- 0.004564354646 -0.836660026534 +/- 0.005976143047 -X.cb(): -12.167000000000 +/- 0.158700000000 -1.728000000000 +/- 0.043200000000 -0.343000000000 +/- 0.014700000000 -X.cbrt(): -1.320006121796 +/- 0.001913052350 -1.062658569183 +/- 0.002951829359 -0.887904001743 +/- 0.004228114294 -X.exp(): -9.974182454815 +/- 0.099741824548 -3.320116922737 +/- 0.033201169227 -2.013752707470 +/- 0.020137527075 -X.log(): -0.832909122935 +/- 0.004347826087 -0.182321556794 +/- 0.008333333333 --0.356674943939 +/- 0.014285714286 -X.sin(): -0.745705212177 +/- 0.006662760213 -0.932039085967 +/- 0.003623577545 -0.644217687238 +/- 0.007648421873 -X.cos(): --0.666276021280 +/- 0.007457052122 -0.362357754477 +/- 0.009320390860 -0.764842187284 +/- 0.006442176872 -X.tan(): --1.119213641734 +/- 0.022526391758 -2.572151622126 +/- 0.076159639672 -0.842288380463 +/- 0.017094497159 -X.sinh(): -4.936961805546 +/- 0.050372206493 -1.509461355412 +/- 0.018106555673 -0.758583701840 +/- 0.012551690056 -X.cosh(): -5.037220649269 +/- 0.049369618055 -1.810655567324 +/- 0.015094613554 -1.255169005631 +/- 0.007585837018 -X.tanh(): -0.980096396266 +/- 0.000394110540 -0.833654607012 +/- 0.003050199962 -0.604367777117 +/- 0.006347395900 diff --git a/test/pyalps/pyhdf5io.output b/test/pyalps/pyhdf5io.output deleted file mode 100644 index 2d30bf3ab..000000000 --- a/test/pyalps/pyhdf5io.output +++ /dev/null @@ -1,39 +0,0 @@ -childs: 23 -/list: array([1, 2, 3], dtype=int32) -/list2: array([[[1, 2], - [3, 4]], - - [[1, 2], - [3, 4]], - - [[1, 2], - [3, 4]], - - [[1, 2], - [3, 4]]], dtype=int32) -/tuple: array([1, 2, 3], dtype=int32) -/dict: [('1', 1), ('4', {'a': array([1, 2, 3]), '(2+3j)': 'foo'}), ('list', array([1, 2, 3], dtype=int32)), ('numpy', array([1, 2, 3])), ('numpycpx', array([1.1+1.j, 0. +2.j, 3.5+0.j])), ('scalar', 1), ('string', 'str')] -/numpy: array([1, 2, 3]) -/numpy2: array([1.1, 2. , 3.5]) -/numpy3: array([1.1+1.j, 0. +2.j, 3.5+0.j]) -/numpyel: 1 -/numpyel2: 1.1 -/numpyel3: (1.1+1j) -/int: 1 -/long: 1 -/double: 1.0 -/complex: (1+1j) -/string: 'str' -/stringlist: ['a', 'list', 'of', 'strings'] -/inhomogenious: [array([1, 2, 3], dtype=int32), array([1, 2, 3]), 'gurke', [[array([1, 2, 3]), 2, 3], ['x', (1+1j)]]] -/inhomogenious2: [array([[1, 2], - [3, 4]], dtype=int32), array([[1, 2], - [3, 4]], dtype=int32), array([[1, 2], - [3, 4]], dtype=int32), [array([1, 2], dtype=int32), array([3], dtype=int32)]] -/inhomogenious3: [array([0, 1, 2]), array([0, 1, 2, 3, 4])] -/inhomogenious4: array([[ 0, 1, 2], - [ 0, 10, 20]]) -/inhomogenious5: [array([0, 1, 2], dtype=int32), array([0, 1, 2, 3, 4], dtype=int32), array([0, 1, 2], dtype=int32)] -/numpylist1: array([[0, 1, 2, 3, 4], - [5, 6, 7, 8, 9]]) -/numpylist2: [array([0, 1, 2, 3, 4]), array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])] diff --git a/test/pyalps/pyparams.output b/test/pyalps/pyparams.output deleted file mode 100644 index fe41df989..000000000 --- a/test/pyalps/pyparams.output +++ /dev/null @@ -1,15 +0,0 @@ -a ok! -b ok! -val1 ok! -val2 ok! -x ok! -a -b -val1 -val2 -x -a ok! -b ok! -val1 ok! -val2 ok! -x ok! diff --git a/test/pyalps/run_python_test.cmake b/test/pyalps/run_python_test.cmake deleted file mode 100644 index f90e20bfd..000000000 --- a/test/pyalps/run_python_test.cmake +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright Matthias Troyer, Synge Todo and Lukas Gamper 2009 - 2010. -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the “Software”), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. - -file(WRITE tmp_${cmd}.sh "PYTHONPATH=\$PYTHONPATH:${pythonpath} ${python_interpreter} ${cmddir}/${cmd}") - -find_file(input_path ${input}.input ${binarydir} ${sourcedir}) -find_file(output_path ${output}.output ${binarydir} ${sourcedir}) - -if(input_path) - execute_process( - COMMAND sh tmp_${cmd}.sh - RESULT_VARIABLE not_successful - INPUT_FILE ${input_path} - OUTPUT_FILE ${cmd}_output - ERROR_VARIABLE err - TIMEOUT 600 - ) -else(input_path) - execute_process( - COMMAND sh tmp_${cmd}.sh - RESULT_VARIABLE not_successful - OUTPUT_FILE ${cmd}_output - ERROR_VARIABLE err - TIMEOUT 600 - ) -endif(input_path) - -file(REMOVE tmp_${cmd}.sh) - -if(not_successful) - message(SEND_ERROR "error runing test 'python_${cmd}': ${err}; shell output: ${not_successful}!") -endif(not_successful) - -if(output_path) - if(WIN32) - configure_file(${cmd}_output ${cmd}_output NEWLINE_STYLE LF) - endif(WIN32) - execute_process( - COMMAND ${CMAKE_COMMAND} -E compare_files ${output_path} ${cmd}_output - RESULT_VARIABLE not_successful - OUTPUT_VARIABLE out - ERROR_VARIABLE err - TIMEOUT 600 - ) - if(not_successful) - message(SEND_ERROR "output does not match for 'python_${cmd}': ${err}; ${out}; shell output: ${not_successful}!") - endif(not_successful) -endif(output_path) - -file(REMOVE ${cmd}_output) From b344bb2f51fa955b9c787bfc4186dce7bee53f71 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 15:56:31 -0500 Subject: [PATCH 17/52] test(pyalps): make the Python suite assert instead of print MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pyhdf5io_test and mcdata_test consisted of print() calls diffed by a CTest harness that no longer exists, so the wheel CI's pytest run passed vacuously — exactly why the HDF5 int->float64 regression went unnoticed. Rewrite both with assertions derived from the historic .output fixtures (which are removed along with the dead run_python_test.cmake), including dtype checks for every list shape the old build distinguished, plus regression cases for bool/mixed/ int64-range lists. Extend test_binding_surface with regression tests for mapping equality and the params value ladder, observable << chaining, save/load overrides reached through C++ virtual dispatch, and in-place accumulator result identity. mcdata's unary minus expectations document a long-standing libalps bug (mcdata::operator-() returns *this unchanged) that the old fixture also recorded; flip them when the C++ operator is fixed. Co-Authored-By: Claude Fable 5 --- test/pyalps/mcdata_test.py | 286 ++++++++++++++-------------- test/pyalps/pyhdf5io_test.py | 219 ++++++++++++++------- test/pyalps/test_binding_surface.py | 115 +++++++++++ 3 files changed, 404 insertions(+), 216 deletions(-) diff --git a/test/pyalps/mcdata_test.py b/test/pyalps/mcdata_test.py index 56bdb7b17..6e7b7b038 100644 --- a/test/pyalps/mcdata_test.py +++ b/test/pyalps/mcdata_test.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -14,152 +13,155 @@ # # **************************************************************************** +# Assertion-based MCScalarData / MCVectorData arithmetic test. The +# expected values are the ones recorded in the historic mcdata.output +# fixture (error propagation without covariance). + from pyalps.alea import * import numpy as np -def str_prec(a): - return '{:.12f}'.format(a) - - -def test_mcdata(): - - print("\nTesting MCScalarData") - print("\n------------------------\n") - - a = MCScalarData(0.81,0.1) - b = MCScalarData(1.21,0.15) - c = MCScalarData(-1.5,0.2) - - print("Initialization:\n") - print("a:\t" + str(a)) - print("b:\t" + str(b)) - print("c:\t" + str(c)) - - print("\n") - - print("Operation:\n") - + +def assert_scalar(value, mean, error): + assert np.isclose(value.mean, mean, rtol=1e-9), (value.mean, mean) + assert np.isclose(value.error, error, rtol=1e-9), (value.error, error) + + +def assert_vector(value, means, errors): + np.testing.assert_allclose(value.mean, means, rtol=1e-9) + np.testing.assert_allclose(value.error, errors, rtol=1e-9) + + +def test_mcdata_scalar(): + b = MCScalarData(1.21, 0.15) + c = MCScalarData(-1.5, 0.2) + + a = MCScalarData(0.81, 0.1) a += b - print("a += b:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - + assert_scalar(a, 2.02, 0.180277563773) + + a = MCScalarData(1.2, 0.1) a -= b - print("a -= b:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - + assert_scalar(a, -0.01, 0.180277563773) + + a = MCScalarData(1.2, 0.1) a *= b - print("a *= b:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - + assert_scalar(a, 1.452, 0.216889372723) + + a = MCScalarData(1.2, 0.1) a /= b - print("a /= b:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - - print("\n") - - a += 2. - print("a += 2.:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - - a -= 2. - print("a -= 2.:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - - a *= 2. - print("a *= 2.:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - - a /= 2. - print("a /= 2.:\t" + str_prec(a)) - a = MCScalarData(1.2,0.1) - - print("\n") - - print("a + b:\t" + str_prec(a / b)) - print("a + 2.:\t" + str_prec(a / 2.)) - print("2. + a:\t" + str_prec(2. / a)) - print("a - b:\t" + str_prec(a / b)) - print("a - 2.:\t" + str_prec(a / 2.)) - print("2. - a:\t" + str_prec(2. / a)) - print("a * b:\t" + str_prec(a / b)) - print("a * 2.:\t" + str_prec(a / 2.)) - print("2. * a:\t" + str_prec(2. / a)) - print("a / b:\t" + str_prec(a / b)) - print("a / 2.:\t" + str_prec(a / 2.)) - print("2. / a:\t" + str_prec(2. / a)) - - print("\n") - - print("-a:\t" + str_prec(-a)) - print("abs(c):\t" + str_prec(abs(c))) - - print("\n") - - print("pow(a,2.71):\t" + str_prec(pow(a,2.71))) - print("a.sq()\t" + str_prec(a.sq())) - print("a.sqrt()\t" + str_prec(a.sqrt())) - print("a.cb()\t" + str_prec(a.cb())) - print("a.cbrt()\t" + str_prec(a.cbrt())) - print("a.exp()\t" + str_prec(a.exp())) - print("a.log()\t" + str_prec(a.log())) - - print("a.sin()\t" + str_prec(a.sin())) - print("a.cos()\t" + str_prec(a.cos())) - print("a.tan()\t" + str_prec(a.tan())) - # print("a.asin()\t" + str_prec(a.asin())) - # print("a.acos()\t" + str_prec(a.acos())) - # print("a.atan()\t" + str_prec(a.atan())) - print("a.tanh()\t" + str_prec(a.tanh())) - - print("\n") - print("\nTesting MCVectorData") - print("\n------------------------\n") - - print("Manipulation\n") - + assert_scalar(a, 0.991735537190, 0.148138359895) + + a = MCScalarData(1.2, 0.1) + a += 2.0 + assert_scalar(a, 3.2, 0.1) + + a = MCScalarData(1.2, 0.1) + a -= 2.0 + assert_scalar(a, -0.8, 0.1) + + a = MCScalarData(1.2, 0.1) + a *= 2.0 + assert_scalar(a, 2.4, 0.2) + + a = MCScalarData(1.2, 0.1) + a /= 2.0 + assert_scalar(a, 0.6, 0.05) + + a = MCScalarData(1.2, 0.1) + assert_scalar(a + b, 2.41, 0.180277563773) + assert_scalar(a - b, -0.01, 0.180277563773) + assert_scalar(a * b, 1.452, 0.216889372723) + assert_scalar(a / b, 0.991735537190, 0.148138359895) + assert_scalar(a + 2.0, 3.2, 0.1) + assert_scalar(a - 2.0, -0.8, 0.1) + assert_scalar(a * 2.0, 2.4, 0.2) + assert_scalar(a / 2.0, 0.6, 0.05) + assert_scalar(2.0 / a, 1.666666666667, 0.138888888889) + + # NOTE: documents a long-standing libalps bug, present in the old + # Boost.Python build too (the historic fixture also shows +1.2): + # mcdata::operator-() (src/alps/alea/mcdata.hpp) negates a copy + # and returns *this unchanged, so unary minus is a no-op. When the + # C++ operator is fixed, flip these expectations to -1.2 / negated + # means. + assert_scalar(-a, 1.2, 0.1) + assert_scalar(abs(c), 1.5, 0.2) + + assert_scalar(pow(a, 2.71), 1.639008390308, 0.370142728145) + assert_scalar(a.sq(), 1.44, 0.24) + assert_scalar(a.sqrt(), 1.095445115010, 0.045643546459) + assert_scalar(a.cb(), 1.728, 0.432) + assert_scalar(a.cbrt(), 1.062658569183, 0.029518293588) + assert_scalar(a.exp(), 3.320116922737, 0.332011692274) + assert_scalar(a.log(), 0.182321556794, 0.083333333333) + assert_scalar(a.sin(), 0.932039085967, 0.036235775448) + assert_scalar(a.cos(), 0.362357754477, 0.093203908597) + assert_scalar(a.tan(), 2.572151622126, 0.761596396721) + assert_scalar(a.tanh(), 0.833654607012, 0.030501999621) + + +def test_mcdata_vector(): X = MCVectorData(np.array([2.3, 1.2, 0.7]), np.array([0.01, 0.01, 0.01])) - Y = X+1. - - print("X:\n" + str_prec(X)) - print("Y:\n" + str_prec(Y)) - - print("X + Y:\n" + str_prec(X+Y)) - print("X + 2.:\n" + str_prec(X+2.)) - print("2. + X:\n" + str_prec(2.+X)) - - print("X + Y:\n" + str_prec(X+Y)) - print("X + 2.:\n" + str_prec(X+2.)) - print("2. + X:\n" + str_prec(2.+X)) - - print("X / Y:\n" + str_prec(X/Y)) - print("X / 2.:\n" + str_prec(X/2.)) - print("2. / X:\n" + str_prec(2./X)) - - print("X / Y:\n" + str_prec(X/Y)) - print("X / 2.:\n" + str_prec(X/2.)) - print("2. / X:\n" + str_prec(2./X)) - - print("-X:\n" + str_prec(-X)) - print("abs(X):\n" + str_prec(X)) - - print("pow(X,2.71):\n" + str_prec(pow(X,2.71))) - print("X.sq():\n" + str_prec(X.sq())) - print("X.sqrt():\n" + str_prec(X.sqrt())) - print("X.cb():\n" + str_prec(X.cb())) - print("X.cbrt():\n" + str_prec(X.cbrt())) - print("X.exp():\n" + str_prec(X.exp())) - print("X.log():\n" + str_prec(X.log())) - - print("X.sin():\n" + str_prec(X.sin())) - print("X.cos():\n" + str_prec(X.cos())) - print("X.tan():\n" + str_prec(X.tan())) - # print("X.asin():\n" + str_prec(X.asin())) - # print("X.acos():\n" + str_prec(X.acos())) - # print("X.atan():\n" + str_prec(X.atan())) - print("X.sinh():\n" + str_prec(X.sinh())) - print("X.cosh():\n" + str_prec(X.cosh())) - print("X.tanh():\n" + str_prec(X.tanh())) - - -if __name__ == '__main__': - test_mcdata() \ No newline at end of file + Y = X + 1.0 + + assert_vector(X, [2.3, 1.2, 0.7], [0.01] * 3) + assert_vector(Y, [3.3, 2.2, 1.7], [0.01] * 3) + + assert_vector(X + Y, [5.6, 3.4, 2.4], [0.014142135624] * 3) + assert_vector(X + 2.0, [4.3, 3.2, 2.7], [0.01] * 3) + assert_vector(2.0 + X, [4.3, 3.2, 2.7], [0.01] * 3) + + assert_vector(X / Y, + [0.696969696970, 0.545454545455, 0.411764705882], + [0.003693697954, 0.005177671110, 0.006361514294]) + assert_vector(X / 2.0, [1.15, 0.6, 0.35], [0.005] * 3) + assert_vector(2.0 / X, + [0.869565217391, 1.666666666667, 2.857142857143], + [0.003780718336, 0.013888888889, 0.040816326531]) + + # unary minus is a no-op — same libalps mcdata bug as in the scalar + # test above; flip to negated means once the C++ operator is fixed + assert_vector(-X, [2.3, 1.2, 0.7], [0.01] * 3) + assert_vector(abs(X), [2.3, 1.2, 0.7], [0.01] * 3) + + assert_vector(pow(X, 2.71), + [9.556138502711, 1.639008390308, 0.380378260851], + [0.112596240619, 0.037014272814, 0.014726072670]) + assert_vector(X.sq(), [5.29, 1.44, 0.49], [0.046, 0.024, 0.014]) + assert_vector(X.sqrt(), + [1.516575088810, 1.095445115010, 0.836660026534], + [0.003296902367, 0.004564354646, 0.005976143047]) + assert_vector(X.cb(), [12.167, 1.728, 0.343], [0.1587, 0.0432, 0.0147]) + assert_vector(X.cbrt(), + [1.320006121796, 1.062658569183, 0.887904001743], + [0.001913052350, 0.002951829359, 0.004228114294]) + assert_vector(X.exp(), + [9.974182454815, 3.320116922737, 2.013752707470], + [0.099741824548, 0.033201169227, 0.020137527075]) + assert_vector(X.log(), + [0.832909122935, 0.182321556794, -0.356674943939], + [0.004347826087, 0.008333333333, 0.014285714286]) + assert_vector(X.sin(), + [0.745705212177, 0.932039085967, 0.644217687238], + [0.006662760213, 0.003623577545, 0.007648421873]) + assert_vector(X.cos(), + [-0.666276021280, 0.362357754477, 0.764842187284], + [0.007457052122, 0.009320390860, 0.006442176872]) + assert_vector(X.tan(), + [-1.119213641734, 2.572151622126, 0.842288380463], + [0.022526391758, 0.076159639672, 0.017094497159]) + assert_vector(X.sinh(), + [4.936961805546, 1.509461355412, 0.758583701840], + [0.050372206493, 0.018106555673, 0.012551690056]) + assert_vector(X.cosh(), + [5.037220649269, 1.810655567324, 1.255169005631], + [0.049369618055, 0.015094613554, 0.007585837018]) + assert_vector(X.tanh(), + [0.980096396266, 0.833654607012, 0.604367777117], + [0.000394110540, 0.003050199962, 0.006347395900]) + + +if __name__ == "__main__": + test_mcdata_scalar() + test_mcdata_vector() + print("SUCCESS") diff --git a/test/pyalps/pyhdf5io_test.py b/test/pyalps/pyhdf5io_test.py index cdae9d723..2ee96a37f 100644 --- a/test/pyalps/pyhdf5io_test.py +++ b/test/pyalps/pyhdf5io_test.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ALPS Project: Algorithms and Libraries for Physics Simulations # @@ -13,100 +12,172 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Assertion-based HDF5 round-trip test. The expectations encode the +# legacy Boost.Python on-disk behaviour recorded in pyhdf5io.output: +# exact-type homogeneous lists keep their element type on disk +# ([1, 2, 3] stays int32), bool/mixed/ragged lists become groups that +# read back as lists, and equal-shape numpy-array lists stack into one +# dataset with numpy's dtype. + +import os +import tempfile + import numpy as np import pyalps.hdf5 as hdf5 -## Python 3 does not have `long` type anymore -import sys -if sys.version_info > (3,): - long = int -def test_hdf5io(): - ar = hdf5.archive('pyngs.h5', 'w') - a = np.array([1, 2, 3]); - b = np.array([1.1, 2.0, 3.5]); - c = np.array([1.1 + 1j, 2.0j, 3.5]); +def _write_all(ar): + a = np.array([1, 2, 3]) + c = np.array([1.1 + 1j, 2.0j, 3.5]) d = {"a": a, 2 + 3j: "foo"} - + ar["/list"] = [1, 2, 3] ar["/list2"] = [[[1, 2], [3, 4]], [[1, 2], [3, 4]], [[1, 2], [3, 4]], [[1, 2], [3, 4]]] ar["/tuple"] = (1, 2, 3) ar["/dict"] = {"scalar": 1, "numpy": a, "numpycpx": c, "list": [1, 2, 3], "string": "str", 1: 1, 4: d} ar["/numpy"] = a - ar["/numpy2"] = b + ar["/numpy2"] = np.array([1.1, 2.0, 3.5]) ar["/numpy3"] = c ar["/numpyel"] = a[0] - ar["/numpyel2"] = b[0] + ar["/numpyel2"] = np.array([1.1, 2.0, 3.5])[0] ar["/numpyel3"] = c[0] ar["/int"] = int(1) - ar["/long"] = long(1) + ar["/long"] = 1 ar["/double"] = float(1) ar["/complex"] = complex(1, 1) ar["/string"] = "str" - ar["/stringlist"] = ['a','list','of','strings'] + ar["/stringlist"] = ['a', 'list', 'of', 'strings'] ar["/inhomogenious"] = [[1, 2, 3], a, "gurke", [[a, 2, 3], ["x", complex(1, 1)]]] ar["/inhomogenious2"] = [[[1, 2], [3, 4]], [[1, 2], [3, 4]], [[1, 2], [3, 4]], [[1, 2], [3]]] ar["/inhomogenious3"] = [np.arange(3), np.arange(5)] ar["/inhomogenious4"] = [np.arange(3), 10 * np.arange(3)] ar["/inhomogenious5"] = [list(range(3)), list(range(5)), list(range(3))] - ar["/numpylist1"] = [np.arange(5), np.arange(5,10)] + ar["/numpylist1"] = [np.arange(5), np.arange(5, 10)] ar["/numpylist2"] = [np.arange(5), np.arange(10)] - - del ar - - ar = hdf5.archive('pyngs.h5', 'r') - - childs = ar.list_children('/') - l1 = ar["/list"] - l2 = ar["/list2"] - t1 = ar["/tuple"] - d1 = ar["/dict"] - n1 = ar["/numpy"] - n2 = ar["/numpy2"] - n3 = ar["/numpy3"] - e1 = ar["/numpyel"] - e2 = ar["/numpyel2"] - e3 = ar["/numpyel3"] - s1 = ar["/int"] - s2 = ar["/long"] - s3 = ar["/double"] - s4 = ar["/complex"] - s5 = ar["/string"] - ls = ar["/stringlist"] - i1 = ar["/inhomogenious"] - i2 = ar["/inhomogenious2"] - i3 = ar["/inhomogenious3"] - i4 = ar["/inhomogenious4"] - i5 = ar["/inhomogenious5"] - nl1 = ar["/numpylist1"] - nl2 = ar["/numpylist2"] - - print("childs: ", len(childs)) - print("/list: ", repr(l1)) - print("/list2: ", repr(l2)) - print("/tuple: ", repr(t1)) - print("/dict: ", repr(list(sorted(d1.items())))) - print("/numpy: ", repr(n1)) - print("/numpy2: ", repr(n2)) - print("/numpy3: ", repr(n3)) - print("/numpyel: ", repr(e1)) - print("/numpyel2: ", repr(e2)) - print("/numpyel3: ", repr(e3)) - print("/int: ", repr(s1)) - print("/long: ", repr(s1)) - print("/double: ", repr(s3)) - print("/complex: ", repr(s4)) - print("/string: ", repr(s5)) - print("/stringlist: ", repr(ls)) - print("/inhomogenious: ", repr(i1)) - print("/inhomogenious2: ", repr(i2)) - print("/inhomogenious3: ", repr(i3)) - print("/inhomogenious4: ", repr(i4)) - print("/inhomogenious5: ", repr(i5)) - print("/numpylist1: ", repr(nl1)) - print("/numpylist2: ", repr(nl2)) - - del ar - -if __name__ == '__main__': + # regression cases for the nanobind save path + ar["/floatlist"] = [1.5, 2.5] + ar["/cplxlist"] = [1 + 1j, 2j] + ar["/boollist"] = [True, False] + ar["/mixedlist"] = [1, 2.5] + ar["/biglist"] = [2 ** 40, 2 ** 41] + + +def _assert_int_array(value, expected, dtype=np.int32): + assert isinstance(value, np.ndarray), repr(value) + assert value.dtype == dtype, "expected %s, got %s" % (dtype, value.dtype) + np.testing.assert_array_equal(value, expected) + + +def test_hdf5io(): + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "pyngs.h5") + ar = hdf5.archive(path, "w") + _write_all(ar) + del ar + + ar = hdf5.archive(path, "r") + + assert len(ar.list_children("/")) == 28 + + # homogeneous int lists/tuples keep the int element type on disk + _assert_int_array(ar["/list"], [1, 2, 3]) + _assert_int_array(ar["/tuple"], [1, 2, 3]) + list2 = ar["/list2"] + assert list2.dtype == np.int32 + assert list2.shape == (4, 2, 2) + np.testing.assert_array_equal(list2, [[[1, 2], [3, 4]]] * 4) + + # dict → group keyed by stringified keys + d = ar["/dict"] + assert sorted(d.keys()) == ["1", "4", "list", "numpy", "numpycpx", "scalar", "string"] + assert d["scalar"] == 1 and d["1"] == 1 and d["string"] == "str" + _assert_int_array(d["list"], [1, 2, 3]) + np.testing.assert_array_equal(d["numpy"], [1, 2, 3]) + np.testing.assert_allclose(d["numpycpx"], [1.1 + 1j, 2.0j, 3.5]) + assert d["4"]["(2+3j)"] == "foo" + np.testing.assert_array_equal(d["4"]["a"], [1, 2, 3]) + + # numpy arrays and scalars round-trip + np.testing.assert_array_equal(ar["/numpy"], [1, 2, 3]) + assert np.issubdtype(ar["/numpy"].dtype, np.integer) + np.testing.assert_allclose(ar["/numpy2"], [1.1, 2.0, 3.5]) + np.testing.assert_allclose(ar["/numpy3"], [1.1 + 1j, 2.0j, 3.5]) + assert ar["/numpyel"] == 1 + assert abs(ar["/numpyel2"] - 1.1) < 1e-12 + assert ar["/numpyel3"] == 1.1 + 1j + + # python scalars keep their types + assert type(ar["/int"]) is int and ar["/int"] == 1 + assert type(ar["/long"]) is int and ar["/long"] == 1 + assert type(ar["/double"]) is float and ar["/double"] == 1.0 + assert type(ar["/complex"]) is complex and ar["/complex"] == 1 + 1j + assert ar["/string"] == "str" + assert ar["/stringlist"] == ['a', 'list', 'of', 'strings'] + + # heterogeneous list → group, read back as a list + i1 = ar["/inhomogenious"] + assert isinstance(i1, list) and len(i1) == 4 + _assert_int_array(i1[0], [1, 2, 3]) + np.testing.assert_array_equal(i1[1], [1, 2, 3]) + assert i1[2] == "gurke" + np.testing.assert_array_equal(i1[3][0][0], [1, 2, 3]) + assert i1[3][0][1] == 2 and i1[3][0][2] == 3 + assert i1[3][1] == ["x", 1 + 1j] + + # rectangular prefix + one ragged entry → group of matrices + i2 = ar["/inhomogenious2"] + assert isinstance(i2, list) and len(i2) == 4 + for entry in i2[:3]: + assert entry.dtype == np.int32 and entry.shape == (2, 2) + np.testing.assert_array_equal(entry, [[1, 2], [3, 4]]) + _assert_int_array(i2[3][0], [1, 2]) + _assert_int_array(i2[3][1], [3]) + + # numpy-array lists: unequal shapes → group; equal shapes → stacked + i3 = ar["/inhomogenious3"] + assert isinstance(i3, list) and len(i3) == 2 + np.testing.assert_array_equal(i3[0], np.arange(3)) + np.testing.assert_array_equal(i3[1], np.arange(5)) + i4 = ar["/inhomogenious4"] + assert isinstance(i4, np.ndarray) and i4.shape == (2, 3) + assert np.issubdtype(i4.dtype, np.integer) + np.testing.assert_array_equal(i4, [[0, 1, 2], [0, 10, 20]]) + i5 = ar["/inhomogenious5"] + assert isinstance(i5, list) and len(i5) == 3 + for entry, size in zip(i5, (3, 5, 3)): + _assert_int_array(entry, np.arange(size)) + nl1 = ar["/numpylist1"] + assert isinstance(nl1, np.ndarray) and nl1.shape == (2, 5) + assert np.issubdtype(nl1.dtype, np.integer) + np.testing.assert_array_equal(nl1, [np.arange(5), np.arange(5, 10)]) + nl2 = ar["/numpylist2"] + assert isinstance(nl2, list) and len(nl2) == 2 + np.testing.assert_array_equal(nl2[0], np.arange(5)) + np.testing.assert_array_equal(nl2[1], np.arange(10)) + + # regression: homogeneous float / complex lists keep their type + fl = ar["/floatlist"] + assert fl.dtype == np.float64 + np.testing.assert_allclose(fl, [1.5, 2.5]) + cl = ar["/cplxlist"] + assert cl.dtype == np.complex128 + np.testing.assert_allclose(cl, [1 + 1j, 2j]) + + # regression: bool and mixed-type lists follow the legacy + # per-element group behaviour instead of silently widening + assert ar["/boollist"] == [True, False] + ml = ar["/mixedlist"] + assert ml == [1, 2.5] + assert type(ml[0]) is int and type(ml[1]) is float + + # regression: out-of-int32-range values widen to int64, not float + bl = ar["/biglist"] + assert np.issubdtype(bl.dtype, np.integer) + np.testing.assert_array_equal(bl, [2 ** 40, 2 ** 41]) + + del ar + + +if __name__ == "__main__": test_hdf5io() + print("SUCCESS") diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 0d48df969..3e54bd13a 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -243,6 +243,117 @@ def test_current_python_numpy_and_scipy_compatibility(monkeypatch): assert isinstance(steady["value"], (bool, np.bool_)) +def test_params_mapping_equality_and_value_ladder(): + from pyalps import ngs + + # MutableMapping equality — lost by the old hasattr-guarded shim, + # present under the Boost.Python __bases__ inheritance + assert ngs.params({"a": 1}) == ngs.params({"a": 1}) + assert ngs.params({"a": 1}) != ngs.params({"a": 2}) + assert ngs.params({"a": 1}) == {"a": 1} + # and, like a Mapping with __eq__, unhashable + try: + hash(ngs.params({})) + raise AssertionError("params must be unhashable") + except TypeError: + pass + + p = ngs.params({}) + # None is rejected with a message that says so + try: + p["x"] = None + raise AssertionError("None must be rejected") + except TypeError as error: + assert "None" in str(error) + # oversized integers raise instead of truncating silently + try: + p["n"] = 2 ** 40 + raise AssertionError("2**40 must be rejected") + except TypeError as error: + assert "32-bit" in str(error) + # exact-type lists round-trip with their element type + p["ilist"] = [1, 2, 3] + assert p["ilist"] == [1, 2, 3] + assert all(type(v) is int for v in p["ilist"]) + p["flist"] = [1.5, 2.5] + assert p["flist"] == [1.5, 2.5] + p["slist"] = ["a", "b"] + assert p["slist"] == ["a", "b"] + # mixed numeric lists widen to double; complex scalars are stored + p["mixed"] = [1, 2.5] + assert p["mixed"] == [1.0, 2.5] + p["cplx"] = 1 + 2j + assert p["cplx"] == 1 + 2j + + +def test_observable_lshift_chains(): + from pyalps import ngs + + observables = ngs.observables() + observables.createRealObservable("chain") + observable = observables["chain"] + returned = (observable << 1.0) << 2.0 + assert returned is observable + assert ngs.observable2result(observable).count == 2 + + +def test_mcbase_save_load_overrides_reach_cpp_dispatch(): + from pyalps import ngs + from pyalps.cxx import pyngshdf5_c + + calls = [] + + class Simulation(ngs.mcbase): + def update(self): + pass + + def measure(self): + pass + + def fraction_completed(self): + return 1.0 + + def save(self, archive): + calls.append("save") + super().save(archive) + + def load(self, archive): + calls.append("load") + super().load(archive) + + simulation = Simulation({"SEED": 42}) + # the base save/load expects a non-empty measurements container + simulation.measurements << ngs.RealObservable("energy") + simulation.measurements["energy"] << 1.0 + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "checkpoint.h5") + archive = pyngshdf5_c.hdf5_archive_impl(path, "w") + # Call through the base binding: this goes through C++ virtual + # dispatch — the same path any C++-side checkpoint takes — and + # must reach the Python override (trampoline forwards save/load). + ngs.mcbase.save(simulation, archive) + del archive + assert calls == ["save"] + + archive = pyngshdf5_c.hdf5_archive_impl(path, "r") + ngs.mcbase.load(simulation, archive) + del archive + assert calls == ["save", "load"] + + +def test_accumulator_result_inplace_identity(): + from pyalps.cxx.pyngsaccumulator_c import error_accumulator + + accumulator = error_accumulator() + accumulator(1.0) + accumulator(2.0) + result = accumulator.result() + alias = result + alias += 1.0 + assert alias is result + assert np.isclose(result.mean(), 2.5) + + def test_python3_property_comparison(monkeypatch): import pyalps import pyalps.apptest as apptest @@ -272,6 +383,10 @@ def GetProperties(self, filenames): test_name_encoding_roundtrip, test_accumulator_surface, test_optional_application_extension_surface, + test_params_mapping_equality_and_value_ladder, + test_observable_lshift_chains, + test_mcbase_save_load_overrides_reach_cpp_dispatch, + test_accumulator_result_inplace_identity, ): test() print("pyalps binding surface: green") From d428723fe332c9da4ff2b97601dace111c8206b5 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 16:10:27 -0500 Subject: [PATCH 18/52] refactor: drop the legacy Boost.Python binding tree The nanobind bindings in bindings/python/pyalps fully replace the Boost.Python modules, but the old sources stayed in the tree: nothing built them, they declared the same module and type names as the shipped extensions, and they could no longer compile anyway because ALPS_HAVE_PYTHON (and paramvalue's boost::python::object variant alternative) is not emitted by any build. Remove them (audit issue 3): - src/alps/python/ and src/alps/ngs/python/ (the module sources) - src/alps/hdf5/python.{hpp,cpp}, src/alps/ngs/boost_python.hpp, src/alps/ngs/detail/{export_sim_to_python,get_numpy_type, extract_from_pyobject}.hpp, src/alps/ngs/lib/get_numpy_type.cpp - src/boost/mpi/module.cpp (the never-built mpi_c source) - applications/qmc/dwa/python/dwa.cpp (superseded by bindings/python/pyalps/cpp/apps/dwa.cpp) - the unbuilt Boost.Python export tutorials (tutorials/ngs/5_export_python, the code-07 export.{cpp,py} files) Collapse the now-unreachable ALPS_HAVE_PYTHON conditionals in the surviving headers and sources (mcanalyze, mcdata, value_with_error, params, paramvalue, paramvalue_reader, scheduler/proto/mcbase), which removes the boost::python::object declarations for good. No reference to boost::python remains outside explanatory comments. Validated: full wheel-deps SDK rebuild, pyalps wheel rebuild against it, and 22/22 Python tests green. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 10 +- src/alps/alea/mcanalyze.hpp | 13 - src/alps/alea/mcdata.hpp | 17 - src/alps/alea/value_with_error.hpp | 11 - src/alps/hdf5/python.cpp | 465 ------------------ src/alps/hdf5/python.hpp | 176 ------- src/alps/ngs/boost_python.hpp | 29 -- src/alps/ngs/detail/export_sim_to_python.hpp | 80 --- src/alps/ngs/detail/extract_from_pyobject.hpp | 98 ---- src/alps/ngs/detail/get_numpy_type.hpp | 57 --- src/alps/ngs/detail/paramvalue.hpp | 14 - src/alps/ngs/detail/paramvalue_reader.hpp | 77 --- src/alps/ngs/lib/get_numpy_type.cpp | 38 -- src/alps/ngs/lib/params.cpp | 25 - src/alps/ngs/lib/paramvalue.cpp | 44 -- src/alps/ngs/params.hpp | 8 - src/alps/ngs/python/accumulator.cpp | 400 --------------- src/alps/ngs/python/api.cpp | 35 -- src/alps/ngs/python/hdf5.cpp | 156 ------ src/alps/ngs/python/mcbase.cpp | 109 ---- src/alps/ngs/python/observable.cpp | 88 ---- src/alps/ngs/python/observables.cpp | 72 --- src/alps/ngs/python/params.cpp | 108 ---- src/alps/ngs/python/random01.cpp | 33 -- src/alps/ngs/python/result.cpp | 193 -------- src/alps/ngs/python/results.cpp | 52 -- src/alps/ngs/scheduler/proto/mcbase.hpp | 15 - src/alps/python/make_copy.hpp | 27 - src/alps/python/numpy_array.cpp | 84 ---- src/alps/python/numpy_array.hpp | 132 ----- src/alps/python/numpy_import.hpp | 63 --- src/alps/python/pyalea.cpp | 439 ----------------- src/alps/python/pymcdata.cpp | 355 ------------- src/alps/python/pytools.cpp | 115 ----- src/alps/python/save_observable_to_hdf5.hpp | 30 -- src/boost/mpi/module.cpp | 55 --- tutorials/code-07-mcmain-mcbase/export.cpp | 22 - tutorials/code-07-mcmain-mcbase/export.py | 61 --- .../heisenberg/o_n_model/export.cpp | 12 - tutorials/ngs/5_export_python/export2py.cpp | 22 - tutorials/ngs/5_export_python/ising.cpp | 102 ---- tutorials/ngs/5_export_python/ising.hpp | 51 -- tutorials/ngs/5_export_python/main.py | 62 --- 43 files changed, 3 insertions(+), 4052 deletions(-) delete mode 100644 src/alps/hdf5/python.cpp delete mode 100644 src/alps/hdf5/python.hpp delete mode 100644 src/alps/ngs/boost_python.hpp delete mode 100644 src/alps/ngs/detail/export_sim_to_python.hpp delete mode 100644 src/alps/ngs/detail/extract_from_pyobject.hpp delete mode 100644 src/alps/ngs/detail/get_numpy_type.hpp delete mode 100644 src/alps/ngs/lib/get_numpy_type.cpp delete mode 100644 src/alps/ngs/python/accumulator.cpp delete mode 100644 src/alps/ngs/python/api.cpp delete mode 100644 src/alps/ngs/python/hdf5.cpp delete mode 100644 src/alps/ngs/python/mcbase.cpp delete mode 100644 src/alps/ngs/python/observable.cpp delete mode 100644 src/alps/ngs/python/observables.cpp delete mode 100644 src/alps/ngs/python/params.cpp delete mode 100644 src/alps/ngs/python/random01.cpp delete mode 100644 src/alps/ngs/python/result.cpp delete mode 100644 src/alps/ngs/python/results.cpp delete mode 100644 src/alps/python/make_copy.hpp delete mode 100644 src/alps/python/numpy_array.cpp delete mode 100644 src/alps/python/numpy_array.hpp delete mode 100644 src/alps/python/numpy_import.hpp delete mode 100644 src/alps/python/pyalea.cpp delete mode 100644 src/alps/python/pymcdata.cpp delete mode 100644 src/alps/python/pytools.cpp delete mode 100644 src/alps/python/save_observable_to_hdf5.hpp delete mode 100644 src/boost/mpi/module.cpp delete mode 100644 tutorials/code-07-mcmain-mcbase/export.cpp delete mode 100644 tutorials/code-07-mcmain-mcbase/export.py delete mode 100644 tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/export.cpp delete mode 100644 tutorials/ngs/5_export_python/export2py.cpp delete mode 100644 tutorials/ngs/5_export_python/ising.cpp delete mode 100644 tutorials/ngs/5_export_python/ising.hpp delete mode 100644 tutorials/ngs/5_export_python/main.py diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index 21bc6bff1..a842a5c1b 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -35,13 +35,9 @@ // pattern becomes a standard trampoline-plus-alias pair. // // Params ingestion: the public alps::mcbase ctor wants an alps::params. -// libalps still declares a params(boost::python::dict) ctor in its -// header, but we don't want to drag boost::python through the -// nanobind bindings. Instead, we convert nb::dict → alps::params at the -// binding boundary through the shared ladder in ../dict_to_params.hpp, -// so mcbase, params and the application modules ingest parameters -// identically. That sidesteps the cross-registry issue and keeps the -// libalps ABI untouched. +// We convert nb::dict → alps::params at the binding boundary through +// the shared ladder in ../dict_to_params.hpp, so mcbase, params and +// the application modules ingest parameters identically. #define PY_ARRAY_UNIQUE_SYMBOL pyngsbase_PyArrayHandle #include #include diff --git a/src/alps/alea/mcanalyze.hpp b/src/alps/alea/mcanalyze.hpp index f4b1bd99d..1fab6d88e 100644 --- a/src/alps/alea/mcanalyze.hpp +++ b/src/alps/alea/mcanalyze.hpp @@ -31,10 +31,6 @@ #include #include -#ifdef ALPS_HAVE_PYTHON - #include - #include -#endif #include @@ -134,9 +130,6 @@ class mctimeseries { // debug constructors. can eventually be deleted. mctimeseries(const std::vector& timeseries):_timeseries(new std::vector(timeseries)) {} -#ifdef ALPS_HAVE_PYTHON - mctimeseries(boost::python::object IN); -#endif // shallow assign void shallow_assign(const mctimeseries& IN) { @@ -165,9 +158,6 @@ class mctimeseries { // get functions inline std::vector timeseries() const {return *_timeseries;} -#ifdef ALPS_HAVE_PYTHON - boost::python::object timeseries_python() const; -#endif void print () const { using alps::numeric::operator<<; @@ -216,9 +206,6 @@ class mctimeseries_view { // this copies the sub-vector. is there a better way? inline std::vector timeseries() const {return std::vector(begin(), end());} -#ifdef ALPS_HAVE_PYTHON - boost::python::object timeseries_python() const; -#endif void print () const { using alps::numeric::operator<<; diff --git a/src/alps/alea/mcdata.hpp b/src/alps/alea/mcdata.hpp index 772c32cf4..8a56bf2ef 100644 --- a/src/alps/alea/mcdata.hpp +++ b/src/alps/alea/mcdata.hpp @@ -60,19 +60,6 @@ #include #include -#ifdef ALPS_HAVE_PYTHON - - #include - - #ifdef tolower - #undef tolower - #endif - - #ifdef toupper - #undef toupper - #endif - -#endif namespace alps { namespace alea { @@ -166,10 +153,6 @@ namespace alps { , error_(error) {} - #ifdef ALPS_HAVE_PYTHON - mcdata(boost::python::object const & mean); - mcdata(boost::python::object const & mean, boost::python::object const & error); - #endif std::size_t size() const { return bins().size();} diff --git a/src/alps/alea/value_with_error.hpp b/src/alps/alea/value_with_error.hpp index 10e4b3069..65eff12e6 100644 --- a/src/alps/alea/value_with_error.hpp +++ b/src/alps/alea/value_with_error.hpp @@ -19,10 +19,6 @@ #include #include -#ifdef ALPS_HAVE_PYTHON -#include -#include -#endif #include #include @@ -53,9 +49,6 @@ namespace alps { public: // constructors, assignment operator -#ifdef ALPS_HAVE_PYTHON - value_with_error(boost::python::object const & mean_nparray, boost::python::object const & error_nparray); -#endif value_with_error(value_type mean =value_type(), value_type error =value_type()) : _mean(mean) , _error(error) @@ -72,10 +65,6 @@ namespace alps { inline value_type mean() const { return _mean; } inline value_type error() const { return _error; } -#ifdef ALPS_HAVE_PYTHON - boost::python::object mean_nparray() const; - boost::python::object error_nparray() const; -#endif // comparison inline bool operator==(value_with_error const & rhs) { return ((_mean == rhs._mean) && (_error == rhs._error)); } diff --git a/src/alps/hdf5/python.cpp b/src/alps/hdf5/python.cpp deleted file mode 100644 index 264181718..000000000 --- a/src/alps/hdf5/python.cpp +++ /dev/null @@ -1,465 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include -#include -#include -#include - -namespace alps { - namespace hdf5 { - - namespace detail { - - template bool is_vectorizable_generic(T const & value) { - static char const * scalar_types[] = { "int", "long", "float", "complex", "str" - , "numpy.str", "numpy.bool", "numpy.int8", "numpy.int16", "numpy.int32", "numpy.int64", "numpy.uint8" - , "numpy.uint16", "numpy.uint32", "numpy.uint64", "numpy.float32", "numpy.float64", "numpy.complex64", "numpy.complex128" }; - using boost::python::len; - using alps::hdf5::get_extent; - boost::python::ssize_t size = len(value); - if (size == 0) - return true; - else { - std::string first_dtype = boost::python::object(value[0]).ptr()->ob_type->tp_name; - bool next_homogenious; - std::vector first_extent; - if (first_dtype == "list") { - if (!is_vectorizable::apply(boost::python::extract(value[0])())) - return false; - first_extent = get_extent(boost::python::extract(value[0])()); - } else if (first_dtype == "tuple") { - if (!is_vectorizable::apply(boost::python::extract(value[0])())) - return false; - first_extent = get_extent(boost::python::extract(value[0])()); - } else if (first_dtype == "numpy.ndarray") - first_extent = get_extent(boost::python::extract(value[0])()); - for(boost::python::ssize_t i = 0; i < size; ++i) { - std::string dtype = boost::python::object(value[i]).ptr()->ob_type->tp_name; - if (dtype == "list") { - if (!is_vectorizable::apply(boost::python::extract(value[i])())) - return false; - std::vector extent = get_extent(boost::python::extract(value[i])()); - if (first_extent.size() != extent.size() || !std::equal(first_extent.begin(), first_extent.end(), extent.begin())) - return false; - } else if (dtype == "tuple") { - if (!is_vectorizable::apply(boost::python::extract(value[i])())) - return false; - std::vector extent = get_extent(boost::python::extract(value[i])()); - if (first_extent.size() != extent.size() || !std::equal(first_extent.begin(), first_extent.end(), extent.begin())) - return false; - } else if (dtype == "numpy.ndarray") { - std::vector extent = get_extent(boost::python::extract(value[i])()); - if (first_extent.size() != extent.size() || !std::equal(first_extent.begin(), first_extent.end(), extent.begin())) - return false; - } else if (first_dtype != dtype || find(scalar_types, scalar_types + 19, dtype) == scalar_types + 19) - return false; - } - return true; - } - } - bool is_vectorizable::apply(boost::python::list const & value) { - return is_vectorizable_generic(value); - } - bool is_vectorizable::apply(boost::python::tuple const & value) { - return is_vectorizable_generic(value); - } - - template std::vector get_extent_generic(T const & value) { - using boost::python::len; - using alps::hdf5::get_extent; - using alps::hdf5::is_vectorizable; - if (!is_vectorizable(value)) - throw archive_error("no rectengual matrix" + ALPS_STACKTRACE); - std::vector extent(1, len(value)); - std::string first_dtype = boost::python::object(value[0]).ptr()->ob_type->tp_name; - if (first_dtype == "list") { - std::vector first_extent(get_extent(boost::python::extract(value[0])())); - copy(first_extent.begin(), first_extent.end(), back_inserter(extent)); - } else if (first_dtype == "tuple") { - std::vector first_extent(get_extent(boost::python::extract(value[0])())); - copy(first_extent.begin(), first_extent.end(), back_inserter(extent)); - } else if (first_dtype == "numpy.ndarray") { - std::vector first_extent = get_extent(boost::python::extract(value[0])()); - copy(first_extent.begin(), first_extent.end(), back_inserter(extent)); - } - return extent; - } - std::vector get_extent::apply(boost::python::list const & value) { - return get_extent_generic(value); - } - std::vector get_extent::apply(boost::python::tuple const & value) { - return get_extent_generic(value); - } - - void set_extent::apply(boost::python::list & value, std::vector const & extent) {} - } - - template void save_generic( - archive & ar - , std::string const & path - , T const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - using alps::cast; - using boost::python::len; - if (ar.is_group(path)) - ar.delete_group(path); - if (len(value) == 0) - ar.write(path, static_cast(NULL), std::vector()); - else if (is_vectorizable(value)) { - size.push_back(len(value)); - chunk.push_back(1); - offset.push_back(0); - for(boost::python::ssize_t i = 0; i < len(value); ++i) { - offset.back() = i; - save(ar, path, boost::python::object(value[i]), size, chunk, offset); - } - } else { - if (ar.is_data(path)) - ar.delete_data(path); - for(boost::python::ssize_t i = 0; i < len(value); ++i) - save(ar, path + "/" + cast(i), boost::python::object(value[i])); - } - } - - void save( - archive & ar - , std::string const & path - , boost::python::list const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - save_generic(ar, path, value, size, chunk, offset); - } - - void save( - archive & ar - , std::string const & path - , boost::python::tuple const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - save_generic(ar, path, value, size, chunk, offset); - } - - void load( - archive & ar - , std::string const & path - , boost::python::list & value - , std::vector chunk - , std::vector offset - ) { - if (ar.is_group(path)) { - std::vector list = ar.list_children(path); - if (list.size()) { - std::vector data; - load(ar, path, data, chunk, offset); - for (std::vector::const_iterator it = data.begin(); it != data.end(); ++it) - value.append(*it); - } - } else if (!ar.is_scalar(path) && ar.is_datatype(path)) { - if (ar.dimensions(path) != 1) - throw archive_error("More than 1 Dimension is not supported." + ALPS_STACKTRACE); - std::vector data; - load(ar, path, data, chunk, offset); - for (std::vector::const_iterator it = data.begin(); it != data.end(); ++it) - value.append(boost::python::str(*it)); - } - } - - namespace detail { - - bool is_vectorizable::apply(alps::python::numpy::array const & value) { - return true; - } - - std::vector get_extent::apply(alps::python::numpy::array const & value) { - if (!is_vectorizable::apply(value)) - throw archive_error("no rectangular matrix" + ALPS_STACKTRACE); - PyArrayObject * ptr = (PyArrayObject *)value.ptr(); - return std::vector(PyArray_DIMS(ptr), PyArray_DIMS(ptr) + PyArray_NDIM(ptr)); - } - - // To set the extent of a numpy array, we need the type, extent is set in load - void set_extent::apply(alps::python::numpy::array & value, std::vector const & extent) {} - - template void load_python_numeric( - archive & ar - , std::string const & path - , alps::python::numpy::array & value - , std::vector chunk - , std::vector offset - , int type - ) { - std::vector extent(ar.extent(path)); - if (ar.is_complex(path)) - extent.pop_back(); - std::vector npextent(extent.begin(), extent.end()); - std::size_t len = std::accumulate(extent.begin(), extent.end(), std::size_t(1), std::multiplies()); - value = alps::python::numpy::from_pyobject(boost::python::object(boost::python::handle<>(PyArray_SimpleNew(npextent.size(), &npextent.front(), type)))); - if (len) { - boost::scoped_ptr raw(new T[len]); - std::pair > data(raw.get(), extent); - load(ar, path, data, chunk, offset); - PyArrayObject * ptr = (PyArrayObject *)value.ptr(); - memcpy(PyArray_DATA(ptr), raw.get(), PyArray_ITEMSIZE(ptr) * PyArray_SIZE(ptr)); - } - } - } - - void save( - archive & ar - , std::string const & path - , alps::python::numpy::array const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - import_numpy(); - if (ar.is_group(path)) - ar.delete_group(path); - PyArrayObject * ptr = (PyArrayObject *)value.ptr(); - if (!PyArray_Check(ptr)) - throw std::runtime_error("invalid numpy data" + ALPS_STACKTRACE); - else if (!PyArray_ISNOTSWAPPED(ptr)) - throw std::runtime_error("numpy array is not native" + ALPS_STACKTRACE); - else if (!(ptr = PyArray_GETCONTIGUOUS(ptr))) // this does Py_INCREF(ptr) - throw std::runtime_error("numpy array cannot be converted to continous array" + ALPS_STACKTRACE); - std::vector extent(PyArray_DIMS(ptr), PyArray_DIMS(ptr) + PyArray_NDIM(ptr)); - std::copy(extent.begin(), extent.end(), std::back_inserter(size)); - std::copy(extent.begin(), extent.end(), std::back_inserter(chunk)); - std::fill_n(std::back_inserter(offset), extent.size(), 0); - if (false); - #define NGS_PYTHON_HDF5_CHECK_NUMPY(T) \ - else if (PyArray_DESCR(ptr)->type_num == ::alps::detail::get_numpy_type(alps::detail::type_wrapper< T >::type())) { \ - save(ar, path, *static_cast< T const *>(PyArray_DATA(ptr)), size, chunk, offset); \ - if (has_complex_elements< T >::value) \ - ar.set_complex(path); \ - } - ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(NGS_PYTHON_HDF5_CHECK_NUMPY) - #undef NGS_PYTHON_HDF5_CHECK_NUMPY - else - throw std::runtime_error("unknown numpy element type" + ALPS_STACKTRACE); - Py_DECREF((PyObject *)ptr); - } - - void load( - archive & ar - , std::string const & path - , alps::python::numpy::array & value - , std::vector chunk - , std::vector offset - ) { - import_numpy(); - if (false); - #define NGS_PYTHON_HDF5_LOAD_NUMPY(T) \ - else if (ar.is_datatype::type>(path) && ar.is_complex(path) == has_complex_elements< T >::value) \ - detail::load_python_numeric< T >(ar, path, value, chunk, offset, ::alps::detail::get_numpy_type(alps::detail::type_wrapper< T >::type())); - ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(NGS_PYTHON_HDF5_LOAD_NUMPY) - #undef NGS_PYTHON_HDF5_LOAD_NUMPY - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - } - - void save( - archive & ar - , std::string const & path - , boost::python::dict const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - if (ar.is_group(path)) - ar.delete_group(path); - const boost::python::list keys = value.keys(); - using boost::python::len; - for (boost::python::ssize_t i = 0; i < len(keys); ++i) { - boost::python::object pyk = keys[i]; - std::string k = boost::python::call_method(pyk.ptr(), "__str__"); - save( - ar - , ar.complete_path(path) + "/" + ar.encode_segment(k) - , value.get(pyk) - ); - } - } - - void load( - archive & ar - , std::string const & path - , boost::python::dict & value - , std::vector chunk - , std::vector offset - ) { - std::vector children = ar.list_children(path); - for (std::vector::const_iterator it = children.begin(); it != children.end(); ++it) { - boost::python::object item; - load(ar, path + "/" + *it, item); - boost::python::call_method(value.ptr(), "__setitem__", *it, item); - } - } - - namespace detail { - - bool is_vectorizable::apply(boost::python::object const & value) { - static char const * scalar_types[] = { "int", "long", "float", "complex", "str" - , "numpy.str", "numpy.bool", "numpy.int8", "numpy.int16", "numpy.int32", "numpy.int64", "numpy.uint8" - , "numpy.uint16", "numpy.uint32", "numpy.uint64", "numpy.float32", "numpy.float64", "numpy.complex64", "numpy.complex128" }; - std::string dtype = value.ptr()->ob_type->tp_name; - if (dtype == "list") - return is_vectorizable::apply(boost::python::extract(value)()); - else if (dtype == "numpy.ndarray") - return is_vectorizable::apply(boost::python::extract(value)()); - return find(scalar_types, scalar_types + 19, dtype) < scalar_types + 19; - } - - std::vector get_extent::apply(boost::python::object const & value) { - using alps::hdf5::get_extent; - std::string dtype = value.ptr()->ob_type->tp_name; - if (!is_vectorizable::apply(value)) - throw archive_error("no rectengual matrix" + ALPS_STACKTRACE); - if (dtype == "list") - return get_extent(boost::python::extract(value)()); - else if (dtype == "numpy.ndarray") - return get_extent(boost::python::extract(value)()); - else - return std::vector(); - } - - void set_extent::apply(boost::python::object & value, std::vector const & extent) {} - - struct save_python_object_visitor { - save_python_object_visitor( - archive & ar - , std::string const & path - , std::vector size - , std::vector chunk - , std::vector offset - ) - : _ar(ar) - , _path(path) - , _size(size) - , _chunk(chunk) - , _offset(offset) - {} - template void operator()(T const & value) { - save(_ar, _path, value, _size, _chunk, _offset); - if (has_complex_elements< T >::value) - _ar.set_complex(_path); - } - template void operator()(T const *, std::vector) { - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - } - archive & _ar; - std::string const & _path; - std::vector _size; - std::vector _chunk; - std::vector _offset; - }; - - template void load_python_object( - archive & ar - , std::string const & path - , boost::python::object & value - , std::vector chunk - , std::vector offset - , int type - ) { - T data; - load(ar, path, data, chunk, offset); - value = boost::python::object(data); - } - } - - void save( - archive & ar - , std::string const & path - , boost::python::object const & value - , std::vector size - , std::vector chunk - , std::vector offset - ) { - std::string dtype = value.ptr()->ob_type->tp_name; - if (dtype == "numpy.ndarray") - save(ar, path, boost::python::extract(value)(), size, chunk, offset); - else if (PyObject_HasAttrString(value.ptr(), "save") && std::string(PyObject_GetAttrString(value.ptr(), "save")->ob_type->tp_name) == "instancemethod") { - std::string context = ar.get_context(); - ar.set_context(ar.complete_path(path)); - boost::python::call_method(value.ptr(), "save", boost::python::object(ar)); - ar.set_context(context); - } else { - using ::alps::detail::extract_from_pyobject; - detail::save_python_object_visitor visitor(ar, path, size, chunk, offset); - extract_from_pyobject(visitor, value); - } - } - - void load( - archive & ar - , std::string const & path - , boost::python::object & value - , std::vector chunk - , std::vector offset - ) { - if (PyObject_HasAttrString(value.ptr(), "load") && std::string(PyObject_GetAttrString(value.ptr(), "load")->ob_type->tp_name) == "MethodType") { - std::string context = ar.get_context(); - ar.set_context(ar.complete_path(path)); - boost::python::call_method(value.ptr(), "load", boost::python::object(ar), path); - ar.set_context(context); - } else if (ar.is_group(path)) { - std::vector list = ar.list_children(path); - bool is_list = list.size(); - for (std::vector::const_iterator it = list.begin(); is_list && it != list.end(); ++it) { - for (std::string::const_iterator jt = it->begin(); is_list && jt != it->end(); ++jt) - if (std::string("1234567890").find_first_of(*jt) == std::string::npos) - is_list = false; - if (is_list && alps::cast(*it) > list.size() - 1) - is_list = false; - } - if (is_list) { - value = boost::python::list(); - load(ar, path, static_cast(value), chunk, offset); - } else { - value = boost::python::dict(); - load(ar, path, static_cast(value), chunk, offset); - } - } else if (ar.is_scalar(path) || (ar.is_datatype(path) && ar.is_complex(path) && ar.extent(path).size() == 1 && ar.extent(path)[0] == 2)) { - if (ar.is_datatype(path)) { - std::string data; - load(ar, path, data, chunk, offset); - value = boost::python::str(data); - #define NGS_PYTHON_HDF5_LOAD_SCALAR_NUMPY(T) \ - } else if (ar.is_datatype::type>(path) && ar.is_complex(path) == has_complex_elements< T >::value) { \ - detail::load_python_object< T >(ar, path, value, chunk, offset, ::alps::detail::get_numpy_type(alps::detail::type_wrapper< T >::type())); - ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(NGS_PYTHON_HDF5_LOAD_SCALAR_NUMPY) - #undef NGS_PYTHON_HDF5_LOAD_SCALAR_NUMPY - } else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - } else if (ar.is_datatype(path)) { - value = boost::python::list(); - load(ar, path, static_cast(value), chunk, offset); - } else { - alps::python::numpy::array array = alps::python::numpy::from_pyobject(boost::python::object()); - load(ar, path, array, chunk, offset); - value = array; - } - } - - } -} diff --git a/src/alps/hdf5/python.hpp b/src/alps/hdf5/python.hpp deleted file mode 100644 index 1fdbecf7b..000000000 --- a/src/alps/hdf5/python.hpp +++ /dev/null @@ -1,176 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_NGS_HDF5_PYTHON_CPP -#define ALPS_NGS_HDF5_PYTHON_CPP - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include - -namespace alps { - namespace hdf5 { - - namespace detail { - - template<> struct is_vectorizable { - static bool apply(boost::python::object const & value); - }; - - template<> struct get_extent { - static std::vector apply(boost::python::object const & value); - }; - - template<> struct set_extent { - static void apply(boost::python::object & value, std::vector const & extent); - }; - } - - ALPS_DECL void save( - archive & ar - , std::string const & path - , boost::python::object const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - ALPS_DECL void load( - archive & ar - , std::string const & path - , boost::python::object & value - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - namespace detail { - - template<> struct is_vectorizable { - static bool apply(boost::python::list const & value); - }; - - template<> struct get_extent { - static std::vector apply(boost::python::list const & value); - }; - - template<> struct set_extent { - static void apply(boost::python::list & value, std::vector const & extent); - }; - } - - ALPS_DECL void save( - archive & ar - , std::string const & path - , boost::python::list const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - ALPS_DECL void load( - archive & ar - , std::string const & path - , boost::python::list & value - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - namespace detail { - - template<> struct is_vectorizable { - static bool apply(boost::python::tuple const & value); - }; - - template<> struct get_extent { - static std::vector apply(boost::python::tuple const & value); - }; - } - - ALPS_DECL void save( - archive & ar - , std::string const & path - , boost::python::tuple const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - namespace detail { - - template<> struct is_vectorizable { - static bool apply(alps::python::numpy::array const & value); - }; - - template<> struct get_extent { - static std::vector apply(alps::python::numpy::array const & value); - }; - - template<> struct set_extent { - // To set the extent of a numpy array, we need the type, extent is set in load - static void apply(alps::python::numpy::array & value, std::vector const & extent); - }; - } - - ALPS_DECL void save( - archive & ar - , std::string const & path - , alps::python::numpy::array const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - ALPS_DECL void load( - archive & ar - , std::string const & path - , alps::python::numpy::array & value - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - ALPS_DECL void save( - archive & ar - , std::string const & path - , boost::python::dict const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - - ALPS_DECL void load( - archive & ar - , std::string const & path - , boost::python::dict & value - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ); - } -} - -#endif diff --git a/src/alps/ngs/boost_python.hpp b/src/alps/ngs/boost_python.hpp deleted file mode 100644 index 676a7ea43..000000000 --- a/src/alps/ngs/boost_python.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -/// WARNING: This header has to be the first include ever! - -#ifndef ALPS_NGS_BOOST_PYTHON_HPP -#define ALPS_NGS_BOOST_PYTHON_HPP - -#include - -#ifdef tolower - #undef tolower -#endif - -#ifdef toupper - #undef toupper -#endif - -#endif diff --git a/src/alps/ngs/detail/export_sim_to_python.hpp b/src/alps/ngs/detail/export_sim_to_python.hpp deleted file mode 100644 index 643f00ba8..000000000 --- a/src/alps/ngs/detail/export_sim_to_python.hpp +++ /dev/null @@ -1,80 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_NGS_DETAIL_EXPORT_SIM_TO_PYTHON_HPP -#define ALPS_NGS_DETAIL_EXPORT_SIM_TO_PYTHON_HPP - -#include - -#include - -#include -#include -#include -#include - -namespace alps { - - template class export2python_wrapper : public T { - - public: - - export2python_wrapper(typename T::parameters_type const & parm, std::size_t seed_offset = 0) - : T(parm, seed_offset) - {} - - typename T::results_type collect_results(typename T::result_names_type const & names = typename T::result_names_type()) { - return names.size() ? T::collect_results(names) : T::collect_results(); - } - - bool run(boost::python::object stop_callback) { - return T::run(boost::bind(&export2python_wrapper::run_helper, this, stop_callback)); - } - - alps::random01 & get_random() { - return T::random; - } - - typename T::parameters_type & get_parameters() { - return T::parameters; - } - - private: - - bool run_helper(boost::python::object stop_callback) { - return boost::python::call(stop_callback.ptr()); - } - }; - -} -BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(collect_results_overloads, collect_results, 0, 1) - -#define ALPS_EXPORT_SIM_TO_PYTHON(NAME, CLASS) \ - boost::python::class_< alps::export2python_wrapper< CLASS >, boost::noncopyable, boost::python::bases >( \ - #NAME , \ - boost::python::init< CLASS ::parameters_type const &, boost::python::optional >() \ - ) \ - .add_property("random", boost::python::make_function( \ - &alps::export2python_wrapper< CLASS >::get_random, boost::python::return_internal_reference<>()) \ - ) \ - .add_property("parameters", boost::python::make_function( \ - &alps::export2python_wrapper< CLASS >::get_parameters, boost::python::return_internal_reference<>()) \ - ) \ - .def("run", static_cast::*)(boost::python::object)>(&alps::export2python_wrapper< CLASS >::run)) \ - .def("resultNames", &alps::export2python_wrapper< CLASS >::result_names) \ - .def("unsavedResultNames", &alps::export2python_wrapper< CLASS >::unsaved_result_names) \ - .def("collectResults", &alps::export2python_wrapper< CLASS >::collect_results, collect_results_overloads(boost::python::args("names"))) \ - .def("save", static_cast::*)(alps::hdf5::archive &) const>(&alps::export2python_wrapper< CLASS >::save)) \ - .def("load", static_cast::*)(alps::hdf5::archive &)>(&alps::export2python_wrapper< CLASS >::load)) - -#endif diff --git a/src/alps/ngs/detail/extract_from_pyobject.hpp b/src/alps/ngs/detail/extract_from_pyobject.hpp deleted file mode 100644 index 24043e936..000000000 --- a/src/alps/ngs/detail/extract_from_pyobject.hpp +++ /dev/null @@ -1,98 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_NGS_DETAIL_EXTRACT_FROM_PYOBJECT_HPP -#define ALPS_NGS_DETAIL_EXTRACT_FROM_PYOBJECT_HPP - -#include -#if defined(ALPS_HAVE_PYTHON) - - #include - #include - - #include - #include - #include - #include - - #include - #include - - #include - - namespace alps { - namespace detail { - - // TODO: move to file and use it in pyngshdf5 - template void extract_from_pyobject(T & visitor, boost::python::object const & data) { - import_numpy(); - std::string dtype = data.ptr()->ob_type->tp_name; - if (dtype == "bool") visitor(boost::python::extract(data)()); - else if (dtype == "int") visitor(boost::python::extract(data)()); - else if (dtype == "long") visitor(boost::python::extract(data)()); - else if (dtype == "float") visitor(boost::python::extract(data)()); - else if (dtype == "complex") visitor(boost::python::extract >(data)()); - else if (dtype == "str") visitor(boost::python::extract(data)()); - else if (dtype == "list") visitor(boost::python::list(data)); - else if (dtype == "tuple") visitor(boost::python::list(data)); - else if (dtype == "dict") visitor(boost::python::dict(data)); - else if (dtype == "numpy.str") visitor(boost::python::call_method(data.ptr(), "__str__")); - else if (dtype == "numpy.bool") visitor(boost::python::call_method(data.ptr(), "__bool__")); - else if (dtype == "numpy.int8") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),Int8))); - else if (dtype == "numpy.int16") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),Int16))); - else if (dtype == "numpy.int32") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),Int32))); - else if (dtype == "numpy.int64") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),Int64))); - else if (dtype == "numpy.uint8") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),UInt8))); - else if (dtype == "numpy.uint16") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),UInt16))); - else if (dtype == "numpy.uint32") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),UInt32))); - else if (dtype == "numpy.uint64") visitor(static_cast(PyArrayScalar_VAL(data.ptr(),UInt64))); - else if (dtype == "numpy.float32") visitor(static_cast(boost::python::call_method(data.ptr(), "__float__"))); - else if (dtype == "numpy.float64") visitor(static_cast(boost::python::call_method(data.ptr(), "__float__"))); - else if (dtype == "numpy.complex64") - visitor(std::complex( - boost::python::call_method(PyObject_GetAttr(data.ptr(), boost::python::str("real").ptr()), "__float__") - , boost::python::call_method(PyObject_GetAttr(data.ptr(), boost::python::str("imag").ptr()), "__float__") - )); - else if (dtype == "numpy.complex128") - visitor(std::complex( - boost::python::call_method(PyObject_GetAttr(data.ptr(), boost::python::str("real").ptr()), "__float__") - , boost::python::call_method(PyObject_GetAttr(data.ptr(), boost::python::str("imag").ptr()), "__float__") - )); - else if (dtype == "numpy.ndarray") { - PyArrayObject * ptr = (PyArrayObject *)data.ptr(); - if (!PyArray_Check(ptr)) - throw std::runtime_error("invalid numpy data" + ALPS_STACKTRACE); - else if (!PyArray_ISNOTSWAPPED(ptr)) - throw std::runtime_error("numpy array is not native" + ALPS_STACKTRACE); - else if (!(ptr = PyArray_GETCONTIGUOUS(ptr))) - throw std::runtime_error("numpy array cannot be converted to continous array" + ALPS_STACKTRACE); - #define ALPS_NGS_EXTRACT_FROM_PYOBJECT_CHECK_NUMPY(T) \ - else if (PyArray_DESCR(ptr)->type_num == detail::get_numpy_type(type_wrapper< T >::type())) \ - visitor( \ - static_cast< T const *>(PyArray_DATA(ptr)) \ - , std::vector(PyArray_DIMS(ptr), PyArray_DIMS(ptr) + PyArray_NDIM(ptr)) \ - ); - ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(ALPS_NGS_EXTRACT_FROM_PYOBJECT_CHECK_NUMPY) - #undef ALPS_NGS_EXTRACT_FROM_PYOBJECT_CHECK_NUMPY - else - throw std::runtime_error("Unknown numpy element type: " + cast(PyArray_DESCR(ptr)->type_num) + ALPS_STACKTRACE); - Py_DECREF((PyObject *)ptr); - } else - throw std::runtime_error("Unsupported type: " + dtype + ALPS_STACKTRACE); - } - } - } - -#endif - -#endif diff --git a/src/alps/ngs/detail/get_numpy_type.hpp b/src/alps/ngs/detail/get_numpy_type.hpp deleted file mode 100644 index 4b9c0bbfc..000000000 --- a/src/alps/ngs/detail/get_numpy_type.hpp +++ /dev/null @@ -1,57 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_NGS_DETAIL_GET_NUMPY_TYPE_HPP -#define ALPS_NGS_DETAIL_GET_NUMPY_TYPE_HPP - -#include - -#if !defined(ALPS_HAVE_PYTHON) - #error numpy is only available if python is enabled -#endif - -#include -#include - -#include - -#define ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(CALLBACK) \ - CALLBACK(bool) \ - CALLBACK(char) \ - CALLBACK(signed char) \ - CALLBACK(unsigned char) \ - CALLBACK(short) \ - CALLBACK(unsigned short) \ - CALLBACK(int) \ - CALLBACK(unsigned) \ - CALLBACK(long) \ - CALLBACK(unsigned long) \ - CALLBACK(long long) \ - CALLBACK(unsigned long long) \ - CALLBACK(float) \ - CALLBACK(double) \ - CALLBACK(long double) \ - CALLBACK(std::complex) \ - CALLBACK(std::complex) \ - CALLBACK(std::complex) - -namespace alps { - namespace detail { - #define ALPS_NGS_DECL_NUMPY_TYPE(T) \ - ALPS_DECL int get_numpy_type(T); - ALPS_NGS_FOREACH_NATIVE_NUMPY_TYPE(ALPS_NGS_DECL_NUMPY_TYPE) - #undef ALPS_NGS_DECL_NUMPY_TYPE - } -} - -#endif diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index 8138cb3eb..624349b5f 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -19,9 +19,6 @@ #include #include -#if defined(ALPS_HAVE_PYTHON) - #include -#endif #include #include @@ -48,14 +45,8 @@ CALLBACK(std::vector) \ CALLBACK(std::vector >) -#if defined(ALPS_HAVE_PYTHON) - #define ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(CALLBACK) \ - ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE_NO_PYTHON(CALLBACK) \ - CALLBACK(boost::python::object) -#else #define ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(CALLBACK) \ ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE_NO_PYTHON(CALLBACK) -#endif namespace alps { @@ -89,11 +80,6 @@ namespace alps { template <> struct paramvalue_index > > { enum { value = 8 }; }; - #if defined(ALPS_HAVE_PYTHON) - template <> struct paramvalue_index { - enum { value = 9 }; - }; - #endif class paramvalue; diff --git a/src/alps/ngs/detail/paramvalue_reader.hpp b/src/alps/ngs/detail/paramvalue_reader.hpp index d0dabce1b..f0db912ed 100644 --- a/src/alps/ngs/detail/paramvalue_reader.hpp +++ b/src/alps/ngs/detail/paramvalue_reader.hpp @@ -19,12 +19,6 @@ #include -#if defined(ALPS_HAVE_PYTHON) - #include - #include - - #include -#endif namespace alps { namespace detail { @@ -39,15 +33,6 @@ namespace alps { throw std::runtime_error(std::string("cannot cast from std::vector<") + typeid(U).name() + "> to " + typeid(T).name() + ALPS_STACKTRACE); } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::list const &) { - throw std::runtime_error(std::string("cannot cast from boost::python::list ") + typeid(T).name() + ALPS_STACKTRACE); - } - - void operator()(boost::python::dict const &) { - throw std::invalid_argument("python dict cannot be used in alps::params" + ALPS_STACKTRACE); - } - #endif T value; }; @@ -66,19 +51,6 @@ namespace alps { (*this)(*it); } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::list const & data) { - for(boost::python::ssize_t i = 0; i < boost::python::len(data); ++i) { - paramvalue_reader_visitor scalar; - extract_from_pyobject(scalar, data[i]); - value.push_back(scalar.value); - } - } - - void operator()(boost::python::dict const &) { - throw std::invalid_argument("python dict cannot be used in alps::params" + ALPS_STACKTRACE); - } - #endif std::vector value; }; @@ -97,16 +69,6 @@ namespace alps { value += (it == ptr ? "," : "") + cast(*it); } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::list const & data) { - for(boost::python::ssize_t i = 0; i < boost::python::len(data); ++i) - value += (value.size() ? "," : "") + boost::python::call_method(boost::python::object(data[i]).ptr(), "__str__"); - } - - void operator()(boost::python::dict const &) { - throw std::invalid_argument("python dict cannot be used in alps::params" + ALPS_STACKTRACE); - } - #endif std::string value; }; @@ -128,11 +90,6 @@ namespace alps { visitor.value = v; } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::object const & v) const { - extract_from_pyobject(visitor, v); - } - #endif T const & get_value() { return visitor.value; @@ -143,40 +100,6 @@ namespace alps { mutable paramvalue_reader_visitor visitor; }; - #if defined(ALPS_HAVE_PYTHON) - template<> struct paramvalue_reader - : public boost::static_visitor<> - { - public: - - template void operator()(U const & v) const { - value = boost::python::object(v); - } - - template void operator()(std::vector const & v) const { - npy_intp npsize = v.size(); - value = boost::python::object(boost::python::handle<>(PyArray_SimpleNew(1, &npsize, detail::get_numpy_type(U())))); - PyArrayObject * ptr = (PyArrayObject *)value.ptr(); - memcpy(PyArray_DATA(ptr), &v.front(), PyArray_ITEMSIZE(ptr) * PyArray_SIZE(ptr)); - } - - void operator()(std::vector const & v) const { - value = boost::python::list(v); - } - - void operator()(boost::python::object const & v) const { - value = v; - } - - boost::python::object const & get_value() { - return value; - } - - private: - - mutable boost::python::object value; - }; - #endif } } diff --git a/src/alps/ngs/lib/get_numpy_type.cpp b/src/alps/ngs/lib/get_numpy_type.cpp deleted file mode 100644 index 4f1cb1d28..000000000 --- a/src/alps/ngs/lib/get_numpy_type.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include - -namespace alps { - namespace detail { - - int get_numpy_type(bool) { return NPY_BOOL; } - int get_numpy_type(char) { return NPY_CHAR; } - int get_numpy_type(unsigned char) { return NPY_UBYTE; } - int get_numpy_type(signed char) { return NPY_BYTE; } - int get_numpy_type(short) { return NPY_SHORT; } - int get_numpy_type(unsigned short) { return NPY_USHORT; } - int get_numpy_type(int) { return NPY_INT; } - int get_numpy_type(unsigned int) { return NPY_UINT; } - int get_numpy_type(long) { return NPY_LONG; } - int get_numpy_type(unsigned long) { return NPY_ULONG; } - int get_numpy_type(long long) { return NPY_LONGLONG; } - int get_numpy_type(unsigned long long) { return NPY_ULONGLONG; } - int get_numpy_type(float) { return NPY_FLOAT; } - int get_numpy_type(double) { return NPY_DOUBLE; } - int get_numpy_type(long double) { return NPY_LONGDOUBLE; } - int get_numpy_type(std::complex) { return NPY_CFLOAT; } - int get_numpy_type(std::complex) { return NPY_CDOUBLE; } - int get_numpy_type(std::complex) { return NPY_CLONGDOUBLE; } - } -} diff --git a/src/alps/ngs/lib/params.cpp b/src/alps/ngs/lib/params.cpp index 0bcbfb81d..2d6a057d0 100644 --- a/src/alps/ngs/lib/params.cpp +++ b/src/alps/ngs/lib/params.cpp @@ -38,31 +38,6 @@ namespace alps { } } - #ifdef ALPS_HAVE_PYTHON - params::params(boost::python::dict const & arg) { - boost::python::extract dict(arg); - if (!dict.check()) - throw std::invalid_argument("parameters can only be created from a dict" + ALPS_STACKTRACE); - const boost::python::list keys = dict().keys(); - for (std::size_t i = 0; i < boost::python::len(keys); ++i) { - boost::python::object pyk = keys[i]; - std::string k = boost::python::call_method(pyk.ptr(), "__str__"); - setter(k, dict().get(pyk)); - } - } - - // TODO: merge with params::params(boost::filesystem::path const & path); - params::params(boost::python::str const & arg) { - std::string path = boost::python::extract(arg)(); - boost::filesystem::ifstream ifs(path); - Parameters par(ifs); - for (Parameters::const_iterator it = par.begin(); it != par.end(); ++it) { - detail::paramvalue val(it->value()); - setter(it->key(), val); - } - } - - #endif std::size_t params::size() const { return keys.size(); diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index 39fa46bab..194b85d47 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -21,39 +21,6 @@ namespace alps { namespace detail { - #if defined(ALPS_HAVE_PYTHON) - struct paramvalue_save_python_visitor { - - paramvalue_save_python_visitor(hdf5::archive & a) - : ar(a) - {} - - template void operator()(U const & data) { - ar[""] << data; - } - - template void operator()(U * const ptr, std::vector const & size) { - ar << make_pvp("", ptr, size); - } - - void operator()(boost::python::list const & raw) { - std::vector data; - for(boost::python::ssize_t i = 0; i < boost::python::len(raw); ++i) { - // TODO: also consider other types than strings ... - paramvalue_reader_visitor scalar; - extract_from_pyobject(scalar, raw[i]); - data.push_back(scalar.value); - } - ar[""] << data; - } - - void operator()(boost::python::dict const &) { - throw std::invalid_argument("python dict cannot be used in alps::params" + ALPS_STACKTRACE); - } - - hdf5::archive & ar; - }; - #endif struct paramvalue_saver: public boost::static_visitor<> { @@ -65,12 +32,6 @@ namespace alps { ar[""] << v; } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::object const & v) const { - paramvalue_save_python_visitor visitor(ar); - extract_from_pyobject(visitor, v); - } - #endif hdf5::archive & ar; }; @@ -84,11 +45,6 @@ namespace alps { os << short_print(v); } - #if defined(ALPS_HAVE_PYTHON) - void operator()(boost::python::object const & v) const { - os << boost::python::call_method(v.ptr(), "__str__"); - } - #endif private: diff --git a/src/alps/ngs/params.hpp b/src/alps/ngs/params.hpp index 674f771d3..d625a107c 100644 --- a/src/alps/ngs/params.hpp +++ b/src/alps/ngs/params.hpp @@ -20,10 +20,6 @@ #include #include -#ifdef ALPS_HAVE_PYTHON - #include - #include -#endif #include #include @@ -64,10 +60,6 @@ namespace alps { params(boost::filesystem::path const &); - #ifdef ALPS_HAVE_PYTHON - params(boost::python::dict const & arg); - params(boost::python::str const & arg); - #endif std::size_t size() const; diff --git a/src/alps/ngs/python/accumulator.cpp b/src/alps/ngs/python/accumulator.cpp deleted file mode 100644 index 62d1d513f..000000000 --- a/src/alps/ngs/python/accumulator.cpp +++ /dev/null @@ -1,400 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include - -#include -#include - -#include -#include - -#include -#include - -namespace alps { - namespace accumulator { - namespace python { - - class object_wrapper { - public: - object_wrapper() {} - template object_wrapper(T arg, typename boost::enable_if >::type* = NULL): obj(arg) {} - object_wrapper(boost::python::object arg): obj(arg) {} - - operator boost::python::object() { return obj; } - operator const boost::python::object() const { return obj; } - - boost::python::object & get() { return obj; } - boost::python::object const &get() const { return obj; } - - void print(std::ostream & os) const { - os << boost::python::call_method(obj.ptr(), "__str__"); - } - - #define ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR(cxxiop, cxxop, iop, op) \ - object_wrapper & cxxiop (object_wrapper const arg) { \ - if (obj == boost::python::object()) \ - obj = arg.obj; \ - else \ - obj iop arg.obj; \ - return *this; \ - } \ - object_wrapper & cxxiop (double arg) { \ - if (obj == boost::python::object()) \ - obj = boost::python::object(arg); \ - else \ - obj iop boost::python::object(arg); \ - return *this; \ - } \ - object_wrapper cxxop (object_wrapper const arg) const { \ - return obj op arg.obj; \ - } \ - object_wrapper cxxop (double arg) const { \ - return obj op boost::python::object(arg); \ - } - ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR(operator+=, operator+, +=, +) - ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR(operator-=, operator-, -=, -) - ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR(operator*=, operator*, *=, *) - ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR(operator/=, operator/, /=, /) - #undef ALPS_ACCUMULATOR_PYTHON_MEMBER_NUMERIC_OPERATOR - - #define ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(cxxop, op) \ - bool cxxop (object_wrapper const arg) const { \ - return obj op arg.obj; \ - } - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator==, ==) - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator!=, !=) - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator<, <) - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator<=, <=) - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator>, >) - ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR(operator>=, >=) - #undef ALPS_ACCUMULATOR_PYTHON_MEMBER_COMARISON_OPERATOR - - object_wrapper operator- () { - return boost::python::call_method(obj.ptr(), "__neg__"); - } - - private: - boost::python::object obj; - }; - - inline std::ostream & operator<<(std::ostream & os, object_wrapper const & arg) { - arg.print(os); - return os; - } - - #define ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR(cxxop, op) \ - inline object_wrapper cxxop (double arg1, object_wrapper const & arg2) { \ - return boost::python::object(arg1) op arg2.get(); \ - } - ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR(operator+, +) - ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR(operator-, -) - ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR(operator*, *) - ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR(operator/, /) - #undef ALPS_ACCUMULATOR_FREE_MEMBER_OPERATOR - - template void magic_call(T & self, boost::python::object arg) { self(object_wrapper(arg)); } - - template std::string magic_str(T & self) { - std::stringstream ss; - self.print(ss); - return ss.str(); - } - - #define ALPS_ACCUMULATOR_PYTHON_FUNCTION(name) \ - object_wrapper name (object_wrapper const & arg) { \ - boost::python::object np = boost::python::import("numpy"); \ - return boost::python::call_method(np.ptr(), #name, arg.get()); \ - } - ALPS_ACCUMULATOR_PYTHON_FUNCTION(sin) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(cos) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(tan) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(sinh) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(cosh) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(tanh) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(asin) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(acos) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(atan) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(abs) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(sqrt) - ALPS_ACCUMULATOR_PYTHON_FUNCTION(log) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(sq) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(cb) - // ALPS_ACCUMULATOR_PYTHON_FUNCTION(cbrt) - #undef ALPS_ACCUMULATOR_PYTHON_FUNCTION - - template typename T::result_type result(T & self) { return typename T::result_type(self); } - - template typename T::result_type neg_result(typename T::result_type self) { self.negate(); return self; } - - template typename T::result_type add_result(typename T::result_type self, typename T::result_type const & arg) { self += arg; return self; } - template typename T::result_type add_double(typename T::result_type self, double arg) { self += arg; return self; } - - template typename T::result_type sub_result(typename T::result_type self, typename T::result_type const & arg) { self -= arg; return self; } - template typename T::result_type sub_double(typename T::result_type self, double arg) { self -= arg; return self; } - template typename T::result_type rsub_double(typename T::result_type self, double arg) { self.negate(); self += arg; return self; } - - template typename T::result_type mul_result(typename T::result_type self, typename T::result_type const & arg) { self *= arg; return self; } - template typename T::result_type mul_double(typename T::result_type self, double arg) { self *= arg; return self; } - - template typename T::result_type div_result(typename T::result_type self, typename T::result_type const & arg) { self /= arg; return self; } - template typename T::result_type div_double(typename T::result_type self, double arg) { self /= arg; return self; } - template typename T::result_type rdiv_double(typename T::result_type self, double arg) { self.inverse(); self *= arg; return self; } - - template typename T::result_type sin(typename T::result_type self) { self.sin(); return self; } - template typename T::result_type cos(typename T::result_type self) { self.cos(); return self; } - template typename T::result_type tan(typename T::result_type self) { self.tan(); return self; } - template typename T::result_type sinh(typename T::result_type self) { self.sinh(); return self; } - template typename T::result_type cosh(typename T::result_type self) { self.cosh(); return self; } - template typename T::result_type tanh(typename T::result_type self) { self.tanh(); return self; } - template typename T::result_type asin(typename T::result_type self) { self.asin(); return self; } - template typename T::result_type acos(typename T::result_type self) { self.acos(); return self; } - template typename T::result_type atan(typename T::result_type self) { self.atan(); return self; } - template typename T::result_type abs(typename T::result_type self) { self.abs(); return self; } - template typename T::result_type sqrt(typename T::result_type self) { self.sqrt(); return self; } - template typename T::result_type log(typename T::result_type self) { self.log(); return self; } - // template typename T::result_type sq(typename T::result_type self) { self.sq(); return self; } - // template typename T::result_type cb(typename T::result_type self) { self.cb(); return self; } - // template typename T::result_type cbrt(typename T::result_type self) { self.cbrt(); return self; } - - } - } - - namespace hdf5 { - - template<> struct scalar_type { - typedef alps::accumulator::python::object_wrapper type; - }; - - namespace detail { - - template<> struct is_vectorizable { - static bool apply(alps::accumulator::python::object_wrapper const & value) { - return is_vectorizable::apply(value.get()); - } - }; - - template<> struct get_extent { - static std::vector apply(alps::accumulator::python::object_wrapper const & value) { - return get_extent::apply(value.get()); - } - }; - - template<> struct set_extent { - static void apply(alps::accumulator::python::object_wrapper & value, std::vector const & extent) { - set_extent::apply(value.get(), extent); - } - }; - } - - ALPS_DECL void save( - archive & ar - , std::string const & path - , alps::accumulator::python::object_wrapper const & value - , std::vector size = std::vector() - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ) { - save(ar, path, value.get(), size, chunk, offset); - } - - ALPS_DECL void load( - archive & ar - , std::string const & path - , alps::accumulator::python::object_wrapper & value - , std::vector chunk = std::vector() - , std::vector offset = std::vector() - ) { - load(ar, path, value.get(), chunk, offset); - } - } - - namespace ngs { - namespace numeric { - template<> struct inf { - operator alps::accumulator::python::object_wrapper const() { - return alps::accumulator::python::object_wrapper(std::numeric_limits::infinity()); - } - }; - } - } -} - -BOOST_PYTHON_MODULE(pyngsaccumulator_c) { - - using namespace boost::python; - using namespace alps::accumulator::impl; - - #define ALPS_ACCUMULATOR_COMMON(accumulator_type) \ - .def("__str__", &alps::accumulator::python::magic_str< accumulator_type >) \ - .def("save", & accumulator_type ::save) \ - .def("load", & accumulator_type ::load) \ - .def("reset", & accumulator_type ::reset) - - #define ALPS_RESULT_COMMON_OPERATORS(accumulator_type) \ - .def(self += accumulator_type ::result_type()) \ - .def(self += int()) \ - .def(self += long()) \ - .def(self += double()) \ - .def("__neg__", &alps::accumulator::python::neg_result< accumulator_type >) \ - .def("__add__", &alps::accumulator::python::add_result< accumulator_type >) \ - .def("__add__", &alps::accumulator::python::add_double< accumulator_type >) \ - .def("__radd__", &alps::accumulator::python::add_double< accumulator_type >) \ - .def(self -= accumulator_type ::result_type()) \ - .def(self -= int()) \ - .def(self -= long()) \ - .def(self -= double()) \ - .def("__sub__", &alps::accumulator::python::sub_result< accumulator_type >) \ - .def("__sub__", &alps::accumulator::python::sub_double< accumulator_type >) \ - .def("__rsub__", &alps::accumulator::python::rsub_double< accumulator_type >) \ - .def(self *= accumulator_type ::result_type()) \ - .def(self *= int()) \ - .def(self *= long()) \ - .def(self *= double()) \ - .def("__mul__", &alps::accumulator::python::mul_result< accumulator_type >) \ - .def("__mul__", &alps::accumulator::python::mul_double< accumulator_type >) \ - .def("__rmul__", &alps::accumulator::python::mul_double< accumulator_type >) \ - .def(self /= accumulator_type ::result_type()) \ - .def(self /= int()) \ - .def(self /= long()) \ - .def(self /= double()) \ - .def("__div__", &alps::accumulator::python::div_result< accumulator_type >) \ - .def("__div__", &alps::accumulator::python::div_double< accumulator_type >) \ - .def("__rdiv__", &alps::accumulator::python::rdiv_double< accumulator_type >) - - #define ALPS_RESULT_COMMON(accumulator_type) \ - ALPS_RESULT_COMMON_OPERATORS(accumulator_type) \ - .def("sin", &alps::accumulator::python::sin< accumulator_type >) \ - .def("cos", &alps::accumulator::python::cos< accumulator_type >) \ - .def("tan", &alps::accumulator::python::tan< accumulator_type >) \ - .def("sinh", &alps::accumulator::python::sinh< accumulator_type >) \ - .def("cosh", &alps::accumulator::python::cosh< accumulator_type >) \ - .def("tanh", &alps::accumulator::python::tanh< accumulator_type >) \ - /*.def("asin", &alps::accumulator::python::asin< accumulator_type >) \ - .def("acos", &alps::accumulator::python::acos< accumulator_type >) \ - .def("atan", &alps::accumulator::python::atan< accumulator_type >)*/ \ - .def("abs", &alps::accumulator::python::abs< accumulator_type >) \ - .def("sqrt", &alps::accumulator::python::sqrt< accumulator_type >) \ - .def("log", &alps::accumulator::python::log< accumulator_type >) \ - /*.def("sq", &alps::accumulator::python::sq< accumulator_type >) \ - .def("cb", &alps::accumulator::python::cb< accumulator_type >) \ - .def("cbrt", &alps::accumulator::python::cbrt< accumulator_type >)*/ - - typedef alps::accumulator::python::object_wrapper python_object; - - typedef Accumulator > count_accumulator_type; - class_("count_accumulator", init<>()) - .def("__call__", &alps::accumulator::python::magic_call) - ALPS_ACCUMULATOR_COMMON(count_accumulator_type) - .def("result", &alps::accumulator::python::result) - - .def("count", &count_accumulator_type::count) - ; - - typedef count_accumulator_type::result_type count_result_type; - class_("count_result", init<>()) - ALPS_ACCUMULATOR_COMMON(count_result_type) - - .def("count", &count_accumulator_type::count) - - ALPS_RESULT_COMMON(count_accumulator_type) - ; - - typedef Accumulator mean_accumulator_type; - class_("mean_accumulator", init<>()) - .def("__call__", &alps::accumulator::python::magic_call) - ALPS_ACCUMULATOR_COMMON(mean_accumulator_type) - .def("result", &alps::accumulator::python::result) - - .def("count", &mean_accumulator_type::count) - .def("mean", &mean_accumulator_type::mean) - ; - - typedef mean_accumulator_type::result_type mean_result_type; - class_("mean_result", init<>()) - ALPS_ACCUMULATOR_COMMON(mean_result_type) - - .def("count", &mean_accumulator_type::count) - .def("mean", &mean_accumulator_type::mean) - - ALPS_RESULT_COMMON(mean_accumulator_type) - ; - - typedef Accumulator error_accumulator_type; - class_("error_accumulator", init<>()) - .def("__call__", &alps::accumulator::python::magic_call) - ALPS_ACCUMULATOR_COMMON(error_accumulator_type) - .def("result", &alps::accumulator::python::result) - - .def("count", &error_accumulator_type::count) - .def("mean", &error_accumulator_type::mean) - .def("error", &error_accumulator_type::error) - ; - - typedef error_accumulator_type::result_type error_result_type; - class_("error_result", init<>()) - ALPS_ACCUMULATOR_COMMON(error_result_type) - - .def("count", &error_accumulator_type::count) - .def("mean", &error_accumulator_type::mean) - .def("error", &error_accumulator_type::error) - - ALPS_RESULT_COMMON(error_accumulator_type) - ; - - typedef Accumulator binning_analysis_accumulator_type; - class_("binning_analysis_accumulator", init<>()) - .def("__call__", &alps::accumulator::python::magic_call) - ALPS_ACCUMULATOR_COMMON(binning_analysis_accumulator_type) - .def("result", &alps::accumulator::python::result) - - .def("count", &binning_analysis_accumulator_type::count) - .def("mean", &binning_analysis_accumulator_type::mean) - .def("error", &binning_analysis_accumulator_type::error) - ; - - typedef binning_analysis_accumulator_type::result_type binning_analysis_result_type; - class_("binning_analysis_result", init<>()) - ALPS_ACCUMULATOR_COMMON(binning_analysis_result_type) - - .def("count", &binning_analysis_accumulator_type::count) - .def("mean", &binning_analysis_accumulator_type::mean) - .def("error", &binning_analysis_accumulator_type::error) - - ALPS_RESULT_COMMON(binning_analysis_accumulator_type) - ; - - typedef Accumulator max_num_binning_accumulator_type; - class_("max_num_binning_accumulator", init<>()) - .def("__call__", &alps::accumulator::python::magic_call) - ALPS_ACCUMULATOR_COMMON(max_num_binning_accumulator_type) - .def("result", &alps::accumulator::python::result) - - .def("count", &max_num_binning_accumulator_type::count) - .def("mean", &max_num_binning_accumulator_type::mean) - .def("error", &max_num_binning_accumulator_type::error) - ; - - typedef max_num_binning_accumulator_type::result_type max_num_binning_result_type; - class_("max_num_binning_result", init<>()) - ALPS_ACCUMULATOR_COMMON(max_num_binning_result_type) - - .def("count", &max_num_binning_accumulator_type::count) - .def("mean", &max_num_binning_accumulator_type::mean) - .def("error", &max_num_binning_accumulator_type::error) - - ALPS_RESULT_COMMON_OPERATORS(max_num_binning_accumulator_type) - ; -} diff --git a/src/alps/ngs/python/api.cpp b/src/alps/ngs/python/api.cpp deleted file mode 100644 index fbfb6488f..000000000 --- a/src/alps/ngs/python/api.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include - -namespace alps { - namespace detail { - - void save_results_export(mcresults const & res, params const & par, alps::hdf5::archive & ar, std::string const & path) { - ar["/parameters"] << par; - if (res.size()) - ar[path] << res; - } - } -} - -BOOST_PYTHON_MODULE(pyngsapi_c) { - - boost::python::def("collectResults", static_cast::type (*)(alps::mcbase const &)>(&alps::collect_results)); - - boost::python::def("saveResults", &alps::detail::save_results_export); - -} diff --git a/src/alps/ngs/python/hdf5.cpp b/src/alps/ngs/python/hdf5.cpp deleted file mode 100644 index fd0a206c2..000000000 --- a/src/alps/ngs/python/hdf5.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace alps { - namespace detail { - - struct std_string_to_python { - static PyObject* convert(std::string const & value) { - return boost::python::incref(boost::python::str(value).ptr()); - } - }; - - struct std_vector_string_to_python { - static PyObject* convert(std::vector const & value) { - boost::python::list result; - for (std::vector::const_iterator it = value.begin(); it != value.end(); ++it) - result.append(boost::python::str(*it)); - return boost::python::incref(result.ptr()); - } - }; - - boost::python::str python_hdf5_get_filename(alps::hdf5::archive & ar) { - return boost::python::str(ar.get_filename()); - } - - void python_hdf5_save(alps::hdf5::archive & ar, std::string const & path, boost::python::object const & data) { - import_numpy(); - ar[path] << data; - } - - boost::python::object python_hdf5_load(alps::hdf5::archive & ar, std::string const & path) { - import_numpy(); - boost::python::object value; - ar[path] >> value; - return value; - } - - boost::python::list python_hdf5_extent(alps::hdf5::archive & ar, std::string const & path) { - boost::python::list result; - std::vector ext = ar.extent(path); - if (ar.is_complex(path)) { - if (ext.size() > 1) - ext.pop_back(); - else - ext.back() = 1; - } - for (std::vector::const_iterator it = ext.begin(); it != ext.end(); ++it) - result.append(*it); - return result; - } - - boost::array exception_type; - - #define TRANSLATE_CPP_ERROR_TO_PYTHON(T, ID) \ - void translate_ ## T (hdf5:: T const & e) { \ - std::string message = std::string(e.what()).substr(0, std::string(e.what()).find_first_of('\n')); \ - PyErr_SetString(exception_type[ID], const_cast(message.c_str())); \ - } - TRANSLATE_CPP_ERROR_TO_PYTHON(archive_error, 0) - TRANSLATE_CPP_ERROR_TO_PYTHON(archive_not_found, 1) - TRANSLATE_CPP_ERROR_TO_PYTHON(archive_closed, 2) - TRANSLATE_CPP_ERROR_TO_PYTHON(invalid_path, 3) - TRANSLATE_CPP_ERROR_TO_PYTHON(path_not_found, 4) - TRANSLATE_CPP_ERROR_TO_PYTHON(wrong_type, 5) - - void register_exception_type(int id, boost::python::object type) { - Py_INCREF(type.ptr()); - exception_type[id] = type.ptr(); - } - } -} - -BOOST_PYTHON_MODULE(pyngshdf5_c) { - - // TODO: move to ownl cpp file and include everywhere - boost::python::to_python_converter< - std::string, - alps::detail::std_string_to_python - >(); - - boost::python::to_python_converter< - std::vector, - alps::detail::std_vector_string_to_python - >(); - - boost::python::register_exception_translator(&alps::detail::translate_archive_error); - boost::python::register_exception_translator(&alps::detail::translate_archive_not_found); - boost::python::register_exception_translator(&alps::detail::translate_archive_closed); - boost::python::register_exception_translator(&alps::detail::translate_invalid_path); - boost::python::register_exception_translator(&alps::detail::translate_path_not_found); - boost::python::register_exception_translator(&alps::detail::translate_wrong_type); - - boost::python::def("register_archive_exception_type", &alps::detail::register_exception_type); - - boost::python::class_( - "hdf5_archive_impl", - boost::python::init() - ) - .def("__deepcopy__", &alps::python::make_copy) - .add_property("filename", &alps::detail::python_hdf5_get_filename) - .add_property("context", &alps::hdf5::archive::get_context) - .add_property("is_open", &alps::hdf5::archive::is_open) - .def("set_context", &alps::hdf5::archive::set_context) - .def("is_group", &alps::hdf5::archive::is_group) - .def("is_data", &alps::hdf5::archive::is_data) - .def("is_attribute", &alps::hdf5::archive::is_attribute) - .def("is_open", &alps::hdf5::archive::is_open) - .def("close", &alps::hdf5::archive::close) - .def("extent", &alps::detail::python_hdf5_extent) - .def("dimensions", &alps::hdf5::archive::dimensions) - .def("is_scalar", &alps::hdf5::archive::is_scalar) - .def("is_complex", &alps::hdf5::archive::is_complex) - .def("is_null", &alps::hdf5::archive::is_null) - .def("list_children", &alps::hdf5::archive::list_children) - .def("list_attributes", &alps::hdf5::archive::list_attributes) - .def("__setitem__", &alps::detail::python_hdf5_save) - .def("__getitem__", &alps::detail::python_hdf5_load) - .def("create_group", &alps::hdf5::archive::create_group) - .def("delete_data", &alps::hdf5::archive::delete_data) - .def("delete_group", &alps::hdf5::archive::delete_group) - .def("delete_attribute", &alps::hdf5::archive::delete_attribute) - ; -} diff --git a/src/alps/ngs/python/mcbase.cpp b/src/alps/ngs/python/mcbase.cpp deleted file mode 100644 index 717535e35..000000000 --- a/src/alps/ngs/python/mcbase.cpp +++ /dev/null @@ -1,109 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyngsbase_PyArrayHandle - -#include - -#include -#include - -#include - -#ifdef ALPS_HAVE_MPI - #include -#endif - -#include -#include -#include -#include - -namespace alps { - - class pymcbase : public mcbase, public boost::python::wrapper { - - public: - - #ifdef ALPS_HAVE_MPI - pymcbase(boost::python::dict arg, std::size_t seed_offset = 42, boost::mpi::communicator = boost::mpi::communicator()) - : mcbase(mcbase::parameters_type(arg), seed_offset) - {} - #else - pymcbase(boost::python::dict arg, std::size_t seed_offset = 42) - : mcbase(mcbase::parameters_type(arg), seed_offset) - {} - #endif - - void update() { - this->get_override("update")(); - } - double fraction_completed() const { - return this->get_override("fraction_completed")(); - } - void measure() { - this->get_override("measure")(); - } - - bool run(boost::python::object stop_callback) { - return mcbase::run(boost::bind(&pymcbase::run_helper, this, stop_callback)); - } - - results_type collect_results(result_names_type const & names = result_names_type()) { - return names.size() ? mcbase::collect_results(names) : mcbase::collect_results(); - } - - alps::random01 & get_random() { - return mcbase::random; - } - - parameters_type & get_parameters() { - return mcbase::parameters; - } - - observable_collection_type & get_measurements() { - return alps::mcbase::measurements; - } - - private: - - bool run_helper(boost::python::object stop_callback) { - return boost::python::call(stop_callback.ptr()); - } - - }; -} - -BOOST_PYTHON_MODULE(pyngsbase_c) { - - boost::python::class_( - "mcbase", - #ifdef ALPS_HAVE_MPI - boost::python::init >() - #else - boost::python::init >() - #endif - ) - .add_property("random", boost::python::make_function(&alps::pymcbase::get_random, boost::python::return_internal_reference<>())) - .add_property("parameters", boost::python::make_function(&alps::pymcbase::get_parameters, boost::python::return_internal_reference<>())) - .add_property("measurements", boost::python::make_function(&alps::pymcbase::get_measurements, boost::python::return_internal_reference<>())) - .def("run", &alps::pymcbase::run) - .def("update", boost::python::pure_virtual(&alps::pymcbase::update)) - .def("measure", boost::python::pure_virtual(&alps::pymcbase::measure)) - .def("fraction_completed", boost::python::pure_virtual(&alps::pymcbase::fraction_completed)) - .def("save", static_cast(&alps::pymcbase::save)) - .def("load", static_cast(&alps::pymcbase::load)) - ; - -} diff --git a/src/alps/ngs/python/observable.cpp b/src/alps/ngs/python/observable.cpp deleted file mode 100644 index 8cc2e5f8b..000000000 --- a/src/alps/ngs/python/observable.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include -#include - -#include -#include - -#include - -#include - -#include - -namespace alps { - namespace detail { - - void observable_append(alps::mcobservable & self, boost::python::object const & data) { - import_numpy(); - if (false); - #define NGS_PYTHON_HDF5_CHECK_SCALAR(N) \ - else if (std::string(data.ptr()->ob_type->tp_name) == N) \ - self << boost::python::extract< double >(data)(); - NGS_PYTHON_HDF5_CHECK_SCALAR("int") - NGS_PYTHON_HDF5_CHECK_SCALAR("long") - NGS_PYTHON_HDF5_CHECK_SCALAR("float") - NGS_PYTHON_HDF5_CHECK_SCALAR("numpy.float64") - else if (std::string(data.ptr()->ob_type->tp_name) == "numpy.ndarray" && PyArray_Check(data.ptr())) { - PyArrayObject * ptr = (PyArrayObject *)data.ptr(); - if (!PyArray_ISNOTSWAPPED(ptr)) - throw std::runtime_error("numpy array is not native" + ALPS_STACKTRACE); - else if (!(ptr = PyArray_GETCONTIGUOUS(ptr))) - throw std::runtime_error("numpy array cannot be converted to continous array" + ALPS_STACKTRACE); - self << std::valarray< double >(static_cast< double const *>(PyArray_DATA(ptr)), *PyArray_DIMS(ptr)); - Py_DECREF((PyObject *)ptr); - } else - throw std::runtime_error("unsupported type"); - } - - void observable_load(alps::mcobservable & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); - } - - alps::mcobservable create_RealObservable_export(std::string name) { - return alps::mcobservable(boost::make_shared(name).get()); - } - - alps::mcobservable create_RealVectorObservable_export(std::string name) { - return alps::mcobservable(boost::make_shared(name).get()); - } - } -} - - -BOOST_PYTHON_MODULE(pyngsobservable_c) { - - boost::python::def("createRealObservable", &alps::detail::create_RealObservable_export); - boost::python::def("createRealVectorObservable", &alps::detail::create_RealVectorObservable_export); - - boost::python::class_( - "observable", - boost::python::no_init - ) - .def("append", &alps::detail::observable_append) - .def("merge", &alps::mcobservable::merge) - .def("save", &alps::mcobservable::save) - .def("load", &alps::detail::observable_load) - .def("addToObservable", &alps::detail::observable_load) - ; - -} - diff --git a/src/alps/ngs/python/observables.cpp b/src/alps/ngs/python/observables.cpp deleted file mode 100644 index fd542ba09..000000000 --- a/src/alps/ngs/python/observables.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyngsobservables_PyArrayHandle - -#include -#include -#include -#include - -#include -#include - -void mcobservables_load(alps::mcobservables & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); -} - -void createRealObservable(alps::mcobservables & self, std::string const & name, boost::uint32_t binnum = 0) { - self << alps::ngs::RealObservable(name, binnum); -} -BOOST_PYTHON_FUNCTION_OVERLOADS(createRealObservable_overloads, createRealObservable, 2, 3) - -void createRealVectorObservable(alps::mcobservables & self, std::string const & name, boost::uint32_t binnum = 0) { - self << alps::ngs::RealVectorObservable(name, binnum); -} -BOOST_PYTHON_FUNCTION_OVERLOADS(createRealVectorObservable_overloads, createRealVectorObservable, 2, 3) - -void addObservable(alps::mcobservables & self, boost::python::object obj) { - boost::python::call_method(obj.ptr(), "addToObservables", boost::ref(self)); -} - -BOOST_PYTHON_MODULE(pyngsobservables_c) { - - boost::python::class_( - "observables", -// boost::python::no_init // Tamama removes this line: Reason: this adds an __init__ method which always raises a Python Runtime exception. - boost::python::init<>() // Tamama add this line. - ) - .def(boost::python::map_indexing_suite()) - .def("reset", &alps::mcobservables::reset) - .def("save", &alps::mcobservables::save) - .def("load", &mcobservables_load) - .def("__lshift__", &addObservable) - .def("createRealObservable", &createRealObservable, createRealObservable_overloads()) - .def("createRealVectorObservable", &createRealVectorObservable, createRealVectorObservable_overloads()) - // TODO: implement! -/* - .def("createRealVectorObservable", &alps::mcobservables::create_RealVectorObservable) - .def("createSimpleRealObservable", &alps::mcobservables::create_SimpleRealObservable) - .def("createSimpleRealVectorObservable", &alps::mcobservables::create_SimpleRealVectorObservable) - .def("createSignedRealObservable", &alps::mcobservables::create_SignedRealObservable) - .def("createSignedRealVectorObservable", &alps::mcobservables::create_SignedRealVectorObservable) - .def("createSignedSimpleRealObservable", &alps::mcobservables::create_SignedSimpleRealObservable) - .def("createSignedSimpleRealVectorObservable", &alps::mcobservables::create_SignedSimpleRealVectorObservable) -*/ - ; - -} diff --git a/src/alps/ngs/python/params.cpp b/src/alps/ngs/python/params.cpp deleted file mode 100644 index e4f2e733e..000000000 --- a/src/alps/ngs/python/params.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyngsparams_PyArrayHandle - -#include -#include - -#include - -#include -#include -#include - -#include -#include - -namespace alps { - namespace detail { - - std::size_t params_len(alps::params const & self) { - return self.size(); - } - - boost::python::object params_getitem(alps::params & self, boost::python::object const & key) { - if (self.defined(boost::python::call_method(key.ptr(), "__str__"))) - return self[boost::python::call_method(key.ptr(), "__str__")].cast(); - else - return boost::python::object(); - } - - void params_setitem(alps::params & self, boost::python::object const & key, boost::python::object & value) { - self[boost::python::call_method(key.ptr(), "__str__")] = value; - } - - void params_delitem(alps::params & self, boost::python::object const & key) { - return self.erase(boost::python::call_method(key.ptr(), "__str__")); - } - - bool params_contains(alps::params & self, boost::python::object const & key) { - return self.defined(boost::python::call_method(key.ptr(), "__str__")); - } - - boost::python::object value_or_default(alps::params & self, boost::python::object const & key, boost::python::object const & value) { - return params_contains(self, key) ? params_getitem(self, key) : value; - } - - void params_load(alps::params & self, alps::hdf5::archive & ar, std::string const & path = "/parameters") { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); - } - BOOST_PYTHON_FUNCTION_OVERLOADS(params_load_overloads, params_load, 2, 3) - - struct param_iterator_to_python { - static PyObject* convert(std::pair const & value) { - return boost::python::incref(boost::python::str(value.first).ptr()); - } - }; - - boost::python::str params_print(alps::params & self) { - std::stringstream ss; - ss << self; - return boost::python::str(ss.str()); - } - - } -} - -BOOST_PYTHON_MODULE(pyngsparams_c) { - - boost::python::to_python_converter< - std::pair, - alps::detail::param_iterator_to_python - >(); - - boost::python::class_( - "params", - boost::python::init >() - ) - .def(boost::python::init >()) - .def(boost::python::init()) - - .def("__len__", &alps::detail::params_len) - .def("__deepcopy__", &alps::python::make_copy) - .def("__getitem__", &alps::detail::params_getitem) - .def("__setitem__", &alps::detail::params_setitem) - .def("__delitem__", &alps::detail::params_delitem) - .def("__contains__", &alps::detail::params_contains) - .def("__iter__", boost::python::iterator()) - .def("__str__", &alps::detail::params_print) - .def("valueOrDefault", &alps::detail::value_or_default) - .def("save", &alps::params::save) - .def("load", &alps::detail::params_load, alps::detail::params_load_overloads()) - ; -} diff --git a/src/alps/ngs/python/random01.cpp b/src/alps/ngs/python/random01.cpp deleted file mode 100644 index 1c3fc2b64..000000000 --- a/src/alps/ngs/python/random01.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2013 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyngsrandom_PyArrayHandle - -#include -#include - -#include - -BOOST_PYTHON_MODULE(pyngsrandom01_c) { - - boost::python::class_( - "random01", - boost::python::init >() - ) - .def("__deepcopy__", &alps::python::make_copy) - .def("__call__", static_cast(&alps::random01::operator())) - .def("save", &alps::random01::save) - .def("load", &alps::random01::load) - ; -} diff --git a/src/alps/ngs/python/result.cpp b/src/alps/ngs/python/result.cpp deleted file mode 100644 index 996d7aa10..000000000 --- a/src/alps/ngs/python/result.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include -#include -#include - -#include -#include -#include - - -namespace alps { - namespace detail { - - template std::string short_print_python(T const & value) { - return cast(value); - } - - template std::string short_print_python(std::vector const & value) { - switch (value.size()) { - case 0: - return "[]"; - case 1: - return "[" + short_print_python(value.front()) + "]"; - case 2: - return "[" + short_print_python(value.front()) + "," + short_print_python(value.back()) + "]"; - default: - return "[" + short_print_python(value.front()) + ",.." + short_print_python(value.size()) + "..," + short_print_python(value.back()) + "]"; - } - } - - boost::python::str mcresult_print(alps::mcresult const & self) { - if (self.count() == 0) - return boost::python::str("No Measurements"); - else if (self.is_type()) - return boost::python::str( - short_print_python(self.mean()) + "(" + short_print_python(self.count()) + ") " - + "+/-" + short_print_python(self.error()) + " " - + short_print_python(self.bins()) + "#" + short_print_python(self.bin_size()) - ); - else if (self.is_type >()) - return boost::python::str( - short_print_python(self.mean >()) + "(" + short_print_python(self.count()) + ") " - + "+/-" + short_print_python(self.error >()) + " " - + short_print_python(self.bins >()) + "#" + short_print_python(self.bin_size()) - ); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - } - - boost::python::object mcresult_mean(alps::mcresult const & self) { - if (self.is_type()) - return boost::python::object(self.mean()); - else if (self.is_type >()) - return alps::python::numpy::convert(self.mean >()); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - return boost::python::object(); - } - - boost::python::object mcresult_error(alps::mcresult const & self) { - if (self.is_type()) - return boost::python::object(self.error()); - else if (self.is_type >()) - return alps::python::numpy::convert(self.error >()); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - return boost::python::object(); - } - - boost::python::object mcresult_tau(alps::mcresult const & self) { - if (self.is_type()) - return boost::python::object(self.tau()); - else if (self.is_type >()) - return alps::python::numpy::convert(self.tau >()); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - return boost::python::object(); - } - - boost::python::object mcresult_variance(alps::mcresult const & self) { - if (self.is_type()) - return boost::python::object(self.variance()); - else if (self.is_type >()) - return alps::python::numpy::convert(self.variance >()); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - return boost::python::object(); - } - - boost::python::object mcresult_bins(alps::mcresult const & self) { - if (self.is_type()) - return alps::python::numpy::convert(self.bins()); -// else if (self.is_type >()) -// return alps::python::numpy::convert(self.bins >()); - else - throw std::runtime_error("Unsupported type." + ALPS_STACKTRACE); - return boost::python::object(); - } - - alps::mcresult observable2result_export(alps::mcobservable const & obs) { - return alps::mcresult(obs); - } - - } -} - -BOOST_PYTHON_MODULE(pyngsresult_c) { - using boost::python::self; - using namespace alps; - - boost::python::def("observable2result", &alps::detail::observable2result_export); - - boost::python::class_( - "result", - boost::python::init >() - ) - .def("__repr__", &alps::detail::mcresult_print) - .def("__deepcopy__", &alps::python::make_copy) - .def("__abs__", static_cast(&abs)) - .def("__pow__", static_cast(&pow)) - - .add_property("mean", &alps::detail::mcresult_mean) - .add_property("error", &alps::detail::mcresult_error) - .add_property("tau", &alps::detail::mcresult_tau) - .add_property("variance", &alps::detail::mcresult_variance) - .add_property("bins", &alps::detail::mcresult_bins) - .add_property("count", &alps::mcresult::count) - - .def(+self) - .def(-self) - .def(self += alps::mcresult()) - .def(self += double()) - .def(self -= alps::mcresult()) - .def(self -= double()) - .def(self *= alps::mcresult()) - .def(self *= double()) - .def(self /= alps::mcresult()) - .def(self /= double()) - .def(self + alps::mcresult()) - .def(alps::mcresult() + self) - .def(self + double()) - .def(double() + self) - .def(self - alps::mcresult()) - .def(alps::mcresult() - self) - .def(self - double()) - .def(double() - self) - .def(self * alps::mcresult()) - .def(alps::mcresult() * self) - .def(self * double()) - .def(double() * self) - .def(self / alps::mcresult()) - .def(alps::mcresult() / self) - .def(self / double()) - .def(double() / self) - - .def("sq", static_cast(&sq)) - .def("cb", static_cast(&cb)) - .def("sqrt", static_cast(&sqrt)) - .def("cbrt", static_cast(&cbrt)) - .def("exp", static_cast(&exp)) - .def("log", static_cast(&log)) - .def("sin", static_cast(&sin)) - .def("cos", static_cast(&cos)) - .def("tan", static_cast(&tan)) - // .def("asin", static_cast(&asin)) - // .def("acos", static_cast(&acos)) - // .def("atan", static_cast(&atan)) - .def("sinh", static_cast(&sinh)) - .def("cosh", static_cast(&cosh)) - .def("tanh", static_cast(&tanh)) -// asinh, aconsh and atanh are not part of C++03 standard -// .def("asinh", static_cast(&asinh)) -// .def("acosh", static_cast(&acosh)) -// .def("atanh", static_cast(&atanh)) - - .def("save", &alps::mcresult::save) - .def("load", &alps::mcresult::load) - ; - -} diff --git a/src/alps/ngs/python/results.cpp b/src/alps/ngs/python/results.cpp deleted file mode 100644 index 429c30e83..000000000 --- a/src/alps/ngs/python/results.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2011 by Lukas Gamper * - * Matthias Troyer * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyngsresults_PyArrayHandle - -#include -#include - -#include -#include - -namespace alps { - namespace detail { - - std::string mcresults_print(alps::mcresults & self) { - std::stringstream sstr; - sstr << self; - return sstr.str(); - } - - void mcresults_load(alps::mcresults & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); - } - } -} - -BOOST_PYTHON_MODULE(pyngsresults_c) { - boost::python::class_( - "results", - boost::python::no_init - ) - .def(boost::python::map_indexing_suite()) - .def("__str__", &alps::detail::mcresults_print) - .def("save", &alps::mcresults::save) - .def("load", &alps::detail::mcresults_load) - ; - -} diff --git a/src/alps/ngs/scheduler/proto/mcbase.hpp b/src/alps/ngs/scheduler/proto/mcbase.hpp index 1114b9009..4cc27e64e 100644 --- a/src/alps/ngs/scheduler/proto/mcbase.hpp +++ b/src/alps/ngs/scheduler/proto/mcbase.hpp @@ -23,9 +23,6 @@ #include // TODO: replace by new alea #include -#ifdef ALPS_HAVE_PYTHON - #include -#endif #include @@ -145,13 +142,6 @@ namespace alps { return !stop_callback(); } - #ifdef ALPS_HAVE_PYTHON - bool run( - boost::python::object stop_callback - ) { - return run(boost::bind(callback_wrapper, stop_callback)); - } - #endif result_names_type result_names() const { result_names_type names; @@ -262,11 +252,6 @@ namespace alps { private: - #ifdef ALPS_HAVE_PYTHON - static bool callback_wrapper(boost::python::object stop_callback) { - return boost::python::call(stop_callback.ptr()); - } - #endif status_type m_status; }; diff --git a/src/alps/python/make_copy.hpp b/src/alps/python/make_copy.hpp deleted file mode 100644 index 79770bf85..000000000 --- a/src/alps/python/make_copy.hpp +++ /dev/null @@ -1,27 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 2010 by Matthias Troyer , -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -/* $Id$ */ - -#ifndef ALPS_PYTHON_MAKE_COPY_HPP -#define ALPS_PYTHON_MAKE_COPY_HPP - -#include -namespace alps { namespace python { - -template -T make_copy(T const& x, boost::python::dict const& ) { return x; } - -} } // end namespace alps::python - -#endif // ALPS_PYTHON_MAKE_COPY_HPP diff --git a/src/alps/python/numpy_array.cpp b/src/alps/python/numpy_array.cpp deleted file mode 100644 index 0bdf11698..000000000 --- a/src/alps/python/numpy_array.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 1994-2010 by Ping Nang Ma , -* Lukas Gamper , -* Matthias Troyer -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -#include -#include - -namespace alps { - namespace python { - namespace numpy { - - alps::python::numpy::array from_pyobject(boost::python::object const & source) - { - #if defined(ALPS_HAVE_BOOST_NUMPY) - return boost::python::numpy::array(source); - #else - return boost::python::numeric::array(source); - #endif - } - - void convert(boost::python::object const & source, std::vector & target) { - import_numpy(); - target.resize(PyArray_Size(source.ptr())); - PyArrayObject * ptr = (PyArrayObject *)source.ptr(); - memcpy(&target.front(), static_cast(PyArray_DATA(ptr)), PyArray_ITEMSIZE(ptr) * target.size()); - } - - alps::python::numpy::array convert(double source) { - return convert(std::vector(1, source)); - } - - std::vector convert(boost::python::object const & source) { - std::vector target; - convert(source, target); - return target; - } - - alps::python::numpy::array convert(std::vector const & source) { - import_numpy(); - npy_intp size = source.size(); - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(1, &size, NPY_DOUBLE))); - void * ptr = PyArray_DATA((PyArrayObject*) obj.ptr()); - memcpy(ptr, &source.front(), PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * size); - return boost::python::extract(obj); - } - - alps::python::numpy::array convert(std::vector > const & source) { - import_numpy(); - npy_intp size[2] = {static_cast(source.size()), static_cast(source[0].size()) }; - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(2, size, NPY_DOUBLE))); - void * ptr = PyArray_DATA((PyArrayObject*) obj.ptr()); - for (std::size_t i = 0; i < source.size(); ++i) - memcpy(static_cast(ptr) + i * size[1], &source[i].front(), PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * size[1]); - return boost::python::extract(obj); - } - - alps::python::numpy::array convert(std::vector > > const & source) { - import_numpy(); - npy_intp size[3] = { - static_cast(source.size()) - , static_cast(source[0].size()) - , static_cast(source[0][0].size()) - }; - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(3, size, NPY_DOUBLE))); - void * ptr = PyArray_DATA((PyArrayObject*) obj.ptr()); - for (std::size_t i = 0; i < source.size(); ++i) - for (std::size_t j = 0; j < source[i].size(); ++j) - memcpy(static_cast(ptr) + i * size[1] * size[2] + j * size[2], &source[i][j].front(), PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * size[2]); - return boost::python::extract(obj); - } - } - } -} diff --git a/src/alps/python/numpy_array.hpp b/src/alps/python/numpy_array.hpp deleted file mode 100644 index c98f7f11c..000000000 --- a/src/alps/python/numpy_array.hpp +++ /dev/null @@ -1,132 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 1994-2010 by Ping Nang Ma , -* Lukas Gamper , -* Matthias Troyer -* Michele Dolfi -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -#ifndef ALPS_PYTHON_NUMPY_ARRAY -#define ALPS_PYTHON_NUMPY_ARRAY - -#include -#include -#include -#include -#include - - -namespace alps { - namespace python { - namespace numpy { - #if defined(ALPS_HAVE_BOOST_NUMPY) - typedef boost::python::numpy::ndarray array; - #else - typedef boost::python::numeric::array array; - #endif - - - ALPS_DECL alps::python::numpy::array from_pyobject(boost::python::object const & source); - - ALPS_DECL void convert(boost::python::object const & source, std::vector & target); - - ALPS_DECL alps::python::numpy::array convert(double source); - - ALPS_DECL alps::python::numpy::array convert(std::vector const & source); - - ALPS_DECL alps::python::numpy::array convert(std::vector > const & source); - - ALPS_DECL alps::python::numpy::array convert(std::vector > > const & source); - - - // for interchanging purpose between numpy array and std::vector - template inline NPY_TYPES getEnum(); - - template <> NPY_TYPES inline getEnum() { return NPY_DOUBLE; } - template <> NPY_TYPES inline getEnum() { return NPY_LONGDOUBLE; } - template <> NPY_TYPES inline getEnum() { return NPY_INT; } - template <> NPY_TYPES inline getEnum() { return NPY_INT; } - template <> NPY_TYPES inline getEnum() { return NPY_INT; } - template <> NPY_TYPES inline getEnum() { return NPY_LONG; } - template <> NPY_TYPES inline getEnum() { return NPY_LONG; } - template <> NPY_TYPES inline getEnum() { return NPY_LONG; } - - template - alps::python::numpy::array convert2numpy(T value) - { - import_numpy(); // ### WARNING: forgetting this will end up in segmentation fault! - - npy_intp arr_size= 1; // ### NOTE: npy_intp is nothing but just signed size_t - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(1, &arr_size, getEnum()))); // ### NOTE: PyArray_SimpleNew is the new version of PyArray_FromDims - void *arr_data= PyArray_DATA((PyArrayObject*) obj.ptr()); - memcpy(arr_data, &value, PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * arr_size); - - return boost::python::extract(obj); - } - - template - alps::python::numpy::array convert2numpy(std::vector const& vec) - { - import_numpy(); // ### WARNING: forgetting this will end up in segmentation fault! - - npy_intp arr_size= vec.size(); // ### NOTE: npy_intp is nothing but just signed size_t - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(1, &arr_size, getEnum()))); // ### NOTE: PyArray_SimpleNew is the new version of PyArray_FromDims - void *arr_data= PyArray_DATA((PyArrayObject*) obj.ptr()); - memcpy(arr_data, &vec.front(), PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * arr_size); - - return boost::python::extract(obj); - } - - template - alps::python::numpy::array convert2numpy(std::valarray vec) - { - import_numpy(); // ### WARNING: forgetting this will end up in segmentation fault! - - npy_intp arr_size= vec.size(); // ### NOTE: npy_intp is nothing but just signed size_t - boost::python::object obj(boost::python::handle<>(PyArray_SimpleNew(1, &arr_size, getEnum()))); // ### NOTE: PyArray_SimpleNew is the new version of PyArray_FromDims - void *arr_data= PyArray_DATA((PyArrayObject*) obj.ptr()); - memcpy(arr_data, &vec[0], PyArray_ITEMSIZE((PyArrayObject*) obj.ptr()) * arr_size); - - return boost::python::extract(obj); - } - - template - std::vector convert2vector(boost::python::object arr) - { - import_numpy(); // ### WARNING: forgetting this will end up in segmentation fault! - - std::size_t vec_size = PyArray_Size(arr.ptr()); - PyArrayObject * ptr = (PyArrayObject *)arr.ptr(); - T * data = (T *) PyArray_DATA(ptr); - - std::vector vec(vec_size); - std::copy(data, data + vec_size, vec.begin()); - return vec; - } - - template - std::valarray convert2valarray(boost::python::object arr) - { - import_numpy(); // ### WARNING: forgetting this will end up in segmentation fault! - - std::size_t vec_size = PyArray_Size(arr.ptr()); - PyArrayObject * ptr = (PyArrayObject *)arr.ptr(); - T * data = (T *) PyArray_DATA(ptr); - std::valarray vec(vec_size); - memcpy(&vec[0],data, PyArray_ITEMSIZE(ptr) * vec_size); - return vec; - } - - } - } -} - -#endif diff --git a/src/alps/python/numpy_import.hpp b/src/alps/python/numpy_import.hpp deleted file mode 100644 index 44df32a30..000000000 --- a/src/alps/python/numpy_import.hpp +++ /dev/null @@ -1,63 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2016 by Lukas Gamper * - * Jan Gukelberger * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_PYTHON_NUMPY_IMPORT_HPP -#define ALPS_PYTHON_NUMPY_IMPORT_HPP - -#include - -#if defined(ALPS_HAVE_BOOST_NUMPY) - #include -#else - #include -#endif - -// Allow callers to pin an earlier API version; default to the latest we have tested. -#ifndef NPY_NO_DEPRECATED_API -#define NPY_NO_DEPRECATED_API NPY_1_11_API_VERSION -#endif -#include - -namespace alps { - namespace { - - // Initialize numpy. - // This function has to be called from each translation unit before any function from the - // numpy C API is used. This function must reside in an anonymous namespace in order to - // ensure that it has internal linkage and that each translation unit ends up with its own - // import_numpy function. - // - // Some resources explaining the numpy madness can be found at the following URLs. - // Synopsis: The numpy API consists of macros that call functions trough a static dispatch - // table. This table needs to be set up by a call to import_array() in each translation - // unit lest the numpy calls segfault. - // https://docs.scipy.org/doc/numpy/reference/c-api.array.html#miscellaneous - // http://stackoverflow.com/a/31973355 - // https://sourceforge.net/p/numpy/mailman/message/5700519/ - void import_numpy() { - static bool inited = false; - if (!inited) { - import_array1((void)0); - #if defined(ALPS_HAVE_BOOST_NUMPY) - boost::python::numpy::initialize(); - #else - boost::python::numeric::array::set_module_and_type("numpy", "ndarray"); - #endif - inited = true; - } - } - } -} - -#endif diff --git a/src/alps/python/pyalea.cpp b/src/alps/python/pyalea.cpp deleted file mode 100644 index 7b8bc6c12..000000000 --- a/src/alps/python/pyalea.cpp +++ /dev/null @@ -1,439 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 1994-2010 by Ping Nang Ma , -* Lukas Gamper , -* Matthias Troyer , -* Maximilian Poprawe -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -/* $Id: pyalea.cpp 3520 2010-04-09 16:49:53Z tamama $ */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include - -using namespace boost::python; - -namespace alps { - namespace alea { - - - template - class WrappedValarrayObservable - { - typedef typename T::value_type::value_type element_type; - public: - WrappedValarrayObservable(const std::string& name, int s=0) - : obs(name,s) - {} - - void operator<<(const boost::python::object& arr) - { - obs << alps::python::numpy::convert2valarray(arr); - } - - std::string representation() const - { - return obs.representation(); - } - - alps::python::numpy::array mean() const - { - return alps::python::numpy::convert2numpy(obs.mean()); - } - - alps::python::numpy::array error() const - { - return alps::python::numpy::convert2numpy(obs.error()); - } - - alps::python::numpy::array tau() const - { - return alps::python::numpy::convert2numpy(obs.tau()); - } - - alps::python::numpy::array variance() const - { - return alps::python::numpy::convert2numpy(obs.variance()); - } - - void save(std::string const & filename) const { - hdf5::archive ar(filename, "a"); - ar["/simulation/results/"+obs.representation()] << obs; - } - - typename T::count_type count() const - { - return obs.count(); - } - - typename T::convergence_type converged_errors() const - { - return obs.converged_errors(); - } - - private: - T obs; - - }; - - - template - value_with_error::value_with_error(boost::python::object const & mean_nparray, boost::python::object const & error_nparray): - _mean(alps::python::numpy::convert2vector(mean_nparray) ), - _error(alps::python::numpy::convert2vector(error_nparray) ) {} - - //boost::python::object value_with_error::mean_nparray() const; - //boost::python::object value_with_error::error_nparray() const; - - template - boost::python::str print_value_with_error(alps::alea::value_with_error const & self) { - return boost::python::str(boost::python::str(self.mean()) + " +/- " + boost::python::str(self.error())); - } - - - #define ALPS_ALEA_FUNCTION_NUMPY_WRAPPER(function_name) \ - template \ - alps::python::numpy::array function_name## _wrapper ( const T& arg1 ) { \ - return alps::python::numpy::convert(function_name( arg1 )) ; \ - } - - ALPS_ALEA_FUNCTION_NUMPY_WRAPPER(mean) - ALPS_ALEA_FUNCTION_NUMPY_WRAPPER(variance) - ALPS_ALEA_FUNCTION_NUMPY_WRAPPER(uncorrelated_error) - ALPS_ALEA_FUNCTION_NUMPY_WRAPPER(binning_error) - - #undef ALPS_ALEA_FUNCTION_NUMPY_WRAPPER - - template - boost::python::str print_to_python (const T& IN) { - std::ostringstream strs; - strs << IN; - return boost::python::str(strs.str()); - } - - template - mctimeseries::mctimeseries (boost::python::object IN):_timeseries(new std::vector( alps::python::numpy::convert2vector(IN) )) {} - - template - boost::python::object mctimeseries::timeseries_python() const {return alps::python::numpy::convert(timeseries());} - - template - boost::python::object mctimeseries_view::timeseries_python() const {return alps::python::numpy::convert(timeseries());} - - - } // ending namespace alea -} // ending namespace alps - -using namespace alps::alea; -using namespace alps::numeric; - -// mcdata docstrings -const char constructor_docstring[] = -"The constructor takes two arguments: a string with the name of the observable " -"and optionally a second integer argument specifying the number of bins to be " -"stored."; - -const char timeseries_constructor_docstring[] = -"The constructor takes two arguments: a string with the name of the observable " -"and optionally a second integer argument specifying the number of entries per " -"bin in the time series."; - -const char observable_docstring[] = -"This class is an ALPS observable class to record results of Monte Carlo " -"measurements and evaluate mean values, error, and autocorrelations."; - -const char timeseries_observable_docstring[] = -"This class is an ALPS observable class to record results of Monte Carlo " -"measurements and evaluate mean values, error, and autocorrelations. " -"It records a full binned time series of measurements, where the number of " -"elements per bin can be specified."; - -const char shift_docstring[] = -"New measurements are added using the left shift operator <<."; - -const char save_docstring[] = -"Save the obseravble into the HDF5 file specified as the argument."; - -const char mean_docstring[] = -"the mean value of all measurements recorded."; - -const char error_docstring[] = -"the error of all measurements recorded."; - -const char tau_docstring[] = -"the autocorrelation time estimate of the recorded measurements."; - -const char variance_docstring[] = -"the variance of all measurements recorded."; - -const char count_docstring[] = -"the number of measurements recorded."; - -const char converged_errors_docstring[] = -" (0 -- data converged ; 1 -- data maybe converged ; 2 -- data not converged) "; - -// mcanalyze docstrings -const char mctimeseries_docstring[] = -"This class is a simple class to store timeseries. It can be used with the free statistical functions in the pyalps.alea module."; - -const char mctimeseries_view_docstring[] = -"This class is a view of a mctimeseries object. It does NOT copy the data so the object used to create this should not be deleted before the created object is deleted."; - -const char mctimeseries_constructor_docstring[] = -"This constructor takes a MCTimeseries Object as argument and creates a reference to its data."; - -const char mctimeseries_view_constructor_docstring[] = -"This constructor takes a MCTimeseriesView Object as argument and copies its reference to the data it is refering to."; - -const char mcdata_constructor_docstring[] = -"This constructor takes a MCData Object as argument. It extracts the timeseries from the object."; - -const char numpy_constructor_docstring[] = -"This constructor takes a numpy array as argument and constructs a timeseries from it."; - -const char mctimeseries_timeseries_docstring[] = -"This returns the timeseries stored in the object as numpy array."; - -const char size_docstring[] = -"This returns the size of the timeseries."; - - -const char std_pair_docstring[] = -"Export of a C++ std::pair"; - -const char mcanalyze_mean_docstring[] = -"Takes any MCTimeseries or MCData object as argument. \n\ -Returns the mean of the timeseries in a MCTimeseries object."; - -const char mcanalyze_variance_docstring[] = -"Takes any MCTimeseries or MCData object as argument. \n\ -Returns the variance of the timeseries in a MCTimeseries object."; - -const char integrated_autocorrelation_time_docstring[] = -"Takes two arguments: A MCTimeseries object of the autocorrelation\nand a StdPairDouble object with a fit of the autocorrelation. \n\ -Returns an estimate of the integrated autocorrelation time\nby summing up the autocorrelation as given and then integrating the tail using the fit."; - -const char running_mean_docstring[] = -"Takes any MCTimeseries or MCData object as argument. \n\ -Returns the running mean of the timeseries in a MCTimeseries object."; - -const char reverse_running_mean_docstring[] = -"Takes any MCTimeseries or MCData object as argument. \n\ -Returns the reverse running mean of the timeseries in a MCTimeseries object."; - -BOOST_PYTHON_MODULE(pyalea_c) { -#define ALPS_PY_EXPORT_VECTOROBSERVABLE(class_name, class_docstring, init_docstring) \ - class_ >( \ - #class_name, class_docstring, init >(init_docstring)) \ - .def("__repr__", &WrappedValarrayObservable< alps:: class_name >::representation) \ - .def("__deepcopy__", &alps::python::make_copy >) \ - .def("__lshift__", &WrappedValarrayObservable< alps::class_name >::operator<<,shift_docstring) \ - .def("save", &WrappedValarrayObservable< alps::class_name >::save,save_docstring) \ - .add_property("mean", &WrappedValarrayObservable< alps::class_name >::mean,mean_docstring) \ - .add_property("error", &WrappedValarrayObservable< alps::class_name >::error,error_docstring) \ - .add_property("tau", &WrappedValarrayObservable< alps::class_name >::tau,tau_docstring) \ - .add_property("variance", &WrappedValarrayObservable< alps::class_name >::variance,variance_docstring) \ - .add_property("count", &WrappedValarrayObservable< alps::class_name >::count,count_docstring) \ - .add_property("converged_errors", &WrappedValarrayObservable< alps::class_name >::converged_errors,converged_errors_docstring) \ - ; - -ALPS_PY_EXPORT_VECTOROBSERVABLE(RealVectorObservable,observable_docstring,constructor_docstring) -ALPS_PY_EXPORT_VECTOROBSERVABLE(RealVectorTimeSeriesObservable,timeseries_observable_docstring,timeseries_constructor_docstring) -#undef ALPS_PY_EXPORT_VECTOROBSERVABLE - -#define ALPS_PY_EXPORT_SIMPLEOBSERVABLE(class_name, class_docstring, init_docstring) \ - class_< alps:: class_name >(#class_name, class_docstring, init >(init_docstring)) \ - .def("__deepcopy__", &alps::python::make_copy) \ - .def("__repr__", &alps:: class_name ::representation) \ - .def("__lshift__", &alps:: class_name ::operator<<,shift_docstring) \ - .def("save", &alps::python::save_observable_to_hdf5,save_docstring) \ - .add_property("mean", &alps:: class_name ::mean,mean_docstring) \ - .add_property("error", static_cast(&alps:: class_name ::error),error_docstring) \ - .add_property("tau",&alps:: class_name ::tau,tau_docstring) \ - .add_property("variance",&alps:: class_name ::variance,variance_docstring) \ - .add_property("count",&alps:: class_name ::count,count_docstring) \ - .add_property("converged_errors", &alps:: class_name ::converged_errors,converged_errors_docstring) \ - ; \ - -ALPS_PY_EXPORT_SIMPLEOBSERVABLE(RealObservable,observable_docstring,timeseries_constructor_docstring) -ALPS_PY_EXPORT_SIMPLEOBSERVABLE(RealTimeSeriesObservable,timeseries_observable_docstring,timeseries_constructor_docstring) - -#undef ALPS_PY_EXPORT_SIMPLEOBSERVABLE - -// mcanalyze export - -#define QUOTEME(x) #x - -#define ALPS_MCANALYZE_EXPORT_MCTIMESERIES_CLASSES(type, name) \ - class_ >( QUOTEME(name) , mctimeseries_docstring) \ - .def(init(numpy_constructor_docstring)) \ - .def(init >(mcdata_constructor_docstring)) \ - .def("timeseries", &alps::alea::mctimeseries< type >::timeseries_python, mctimeseries_timeseries_docstring) \ - .add_property("size", &alps::alea::mctimeseries< type >::size, size_docstring) \ - .def("__repr__", &alps::alea::print_to_python >) \ - ; \ - \ - class_ >( QUOTEME(name##View), mctimeseries_view_docstring, init< alps::alea::mctimeseries< type > >(mctimeseries_constructor_docstring)) \ - .def(init< alps::alea::mctimeseries_view< type > >(mctimeseries_view_constructor_docstring)) \ - .def("timeseries", &alps::alea::mctimeseries_view< type >::timeseries_python, numpy_constructor_docstring) \ - .add_property("size", &alps::alea::mctimeseries_view< type >::size, size_docstring) \ - .def("__repr__", &alps::alea::print_to_python >) \ - ; - - -#define ALPS_MCANALYZE_EXPORT_HELPER(templateparms, function_name_py, function_name_c, docstring) \ - def( QUOTEME ( function_name_py ), function_name_c templateparms , docstring); - -#define ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR(container_type, function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < container_type < double > > , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < container_type < std::vector < double > > > , function_name_py, function_name_c , docstring) -/* ALPS_MCANALYZE_EXPORT_HELPER( < container_type < int > > , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < container_type < std::vector < int > > > , function_name_py, function_name_c , docstring)*/ - -#define ALPS_MCANALYZE_EXPORT_SCALAR_ONLY(container_type, function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < container_type < double > > , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_VECTOR_ONLY(container_type, function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < container_type < std::vector < double > > > , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_VALUETYPE_FUNCTION(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < double > , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < std::vector < double > > , function_name_py, function_name_c , docstring) -/* ALPS_MCANALYZE_EXPORT_HELPER( < int > , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_HELPER( < std::vector < int > > , function_name_py, function_name_c , docstring)*/ - - -#define ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR( alps::alea::mcdata , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR( alps::alea::mctimeseries , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR( alps::alea::mctimeseries_view , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_ONLY( alps::alea::mcdata , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_ONLY( alps::alea::mctimeseries , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_ONLY( alps::alea::mctimeseries_view , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_VECTOR_ONLY( alps::alea::mcdata , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_VECTOR_ONLY( alps::alea::mctimeseries , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_VECTOR_ONLY( alps::alea::mctimeseries_view , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_AND_VECTOR(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR( alps::alea::mctimeseries , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR( alps::alea::mctimeseries_view , function_name_py, function_name_c , docstring) - -#define ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_ONLY(function_name_py, function_name_c, docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_ONLY( alps::alea::mctimeseries , function_name_py, function_name_c , docstring) \ - ALPS_MCANALYZE_EXPORT_SCALAR_ONLY( alps::alea::mctimeseries_view , function_name_py, function_name_c , docstring) - - - -docstring_options doc_options; // complete docstring - -class_ >( "ValueWithError", init< optional >() ) - .add_property("mean", &alps::alea::value_with_error::mean) - .add_property("error", &alps::alea::value_with_error::error) - .def("__repr__", &alps::alea::print_value_with_error) -; - -class_ > ( "StdPairDouble", std_pair_docstring, init() ) - .def_readwrite("first", &std::pair::first) - .def_readwrite("second", &std::pair::second) -; - - -doc_options.disable_cpp_signatures(); // no cpp signatures - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(size, alps::size, size_docstring) - - // need scalar and vector seperate so that scalar -> float, vector -> numpy -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(mean, alps::alea::mean, mcanalyze_mean_docstring) -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY(mean, alps::alea::mean_wrapper, mcanalyze_mean_docstring) - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(variance, alps::alea::variance, mcanalyze_variance_docstring) -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY(variance, alps::alea::variance_wrapper, mcanalyze_variance_docstring) - -ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_ONLY(integrated_autocorrelation_time, alps::alea::integrated_autocorrelation_time, integrated_autocorrelation_time_docstring) - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(running_mean, alps::alea::running_mean, running_mean_docstring) -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(reverse_running_mean, alps::alea::reverse_running_mean, reverse_running_mean_docstring) - -ALPS_MCANALYZE_EXPORT_MCTIMESERIES_CLASSES(double, MCScalarTimeseries) -class_ > >( "MCVectorTimeseries" , mctimeseries_docstring) - .def(init > >(mcdata_constructor_docstring)) - .def("timeseries", &alps::alea::mctimeseries< std::vector >::timeseries_python, mctimeseries_timeseries_docstring) - .add_property("size", &alps::alea::mctimeseries< std::vector >::size, size_docstring) - .def("__repr__", &alps::alea::print_to_python > >) - ; - - class_ > >( "MCVectorTimeseriesView", mctimeseries_view_docstring, init< alps::alea::mctimeseries< std::vector > >(mctimeseries_constructor_docstring)) - .def(init< alps::alea::mctimeseries_view< std::vector > >(mctimeseries_view_constructor_docstring)) - .def("timeseries", &alps::alea::mctimeseries_view< std::vector >::timeseries_python, numpy_constructor_docstring) - .add_property("size", &alps::alea::mctimeseries_view< std::vector >::size, size_docstring) - .def("__repr__", &alps::alea::print_to_python > >) - ; -//ALPS_MCANALYZE_EXPORT_MCTIMESERIES_CLASSES(alps::alea::value_with_error, MCScalarTimeseriesWithError) - - -doc_options.disable_all(); // no doc - - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(autocorrelation_distance, alps::alea::autocorrelation_distance, "") -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(autocorrelation_limit, alps::alea::autocorrelation_limit, "") - -ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_ONLY(exponential_autocorrelation_time_distance, alps::alea::exponential_autocorrelation_time_distance, "") -ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_ONLY(exponential_autocorrelation_time_limit, alps::alea::exponential_autocorrelation_time_limit, "") - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(cut_head_distance, alps::alea::cut_head_distance, "") -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(cut_head_limit, alps::alea::cut_head_limit, "") - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR(cut_tail_distance, alps::alea::cut_tail_distance, "") -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(cut_tail_limit, alps::alea::cut_tail_limit, "") - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(uncorrelated_error, alps::alea::uncorrelated_error, "") -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY(uncorrelated_error, alps::alea::uncorrelated_error_wrapper, "") - -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY(binning_error, alps::alea::binning_error, "") -ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY(binning_error, alps::alea::binning_error_wrapper, "") - - -#undef QUOTEME - -#undef ALPS_MCANALYZE_EXPORT_HELPER -#undef ALPS_MCANALYZE_EXPORT_SCALAR_AND_VECTOR -#undef ALPS_MCANALYZE_EXPORT_SCALAR_ONLY -#undef ALPS_MCANALYZE_EXPORT_VECTOR_ONLY -#undef ALPS_MCANALYZE_EXPORT_VALUETYPE_FUNCTION -#undef ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_AND_VECTOR -#undef ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_SCALAR_ONLY -#undef ALPS_MCANALYZE_EXPORT_TIMESERIES_FUNCTION_VECTOR_ONLY -#undef ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_AND_VECTOR -#undef ALPS_MCANALYZE_EXPORT_MCTIMESERIES_FUNCTION_SCALAR_ONLY - -#undef ALPS_MCANALYZE_EXPORT_MCTIMESERIES_CLASSES - -} - diff --git a/src/alps/python/pymcdata.cpp b/src/alps/python/pymcdata.cpp deleted file mode 100644 index 43814b57a..000000000 --- a/src/alps/python/pymcdata.cpp +++ /dev/null @@ -1,355 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 1994-2010 by Ping Nang Ma , -* Lukas Gamper , -* Matthias Troyer -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -/* $Id: pyalea.cpp 3520 2010-04-09 16:49:53Z tamama $ */ - -#define PY_ARRAY_UNIQUE_SYMBOL pyalea_PyArrayHandle - -#include -#include -#include - -#include - -namespace alps { - namespace alea { - - template - mcdata::mcdata(boost::python::object const & mean) - : count_(1) - , binsize_(0) - , max_bin_number_(0) - , data_is_analyzed_(true) - , jacknife_bins_valid_(true) - , cannot_rebin_(false) - { - alps::python::numpy::convert(mean, mean_); - } - - template - mcdata::mcdata(boost::python::object const & mean, boost::python::object const & error) - : count_(1) - , binsize_(0) - , max_bin_number_(0) - , data_is_analyzed_(true) - , jacknife_bins_valid_(true) - , cannot_rebin_(false) - { - alps::python::numpy::convert(mean, mean_); - alps::python::numpy::convert(error, error_); - } - } - - namespace python { - - template std::size_t size(alps::alea::mcdata & data) { - return data.mean().size(); - } - - template boost::python::object get_item(boost::python::back_reference &> data, PyObject* i) { - if (PySlice_Check(i)) { - PySliceObject * slice = static_cast(static_cast(i)); - if (Py_None != slice->step) { - PyErr_SetString(PyExc_IndexError, "slice step size not supported."); - boost::python::throw_error_already_set(); - } - long from = (Py_None == slice->start ? 0 : boost::python::extract(slice->start)()); - if (from < 0) - from += size(data.get()); - from = std::max(std::min(from, size(data.get())), 0); - long to = (Py_None == slice->stop ? 0 : boost::python::extract(slice->stop)()); - if (to < 0) - to += size(data.get()); - to = std::max(std::min(to, size(data.get())), 0); - if (from > to) - return boost::python::object(alps::alea::mcdata()); - else - return boost::python::object(alps::alea::mcdata(data.get(), from, to)); - } else { - long index = 0; - if (boost::python::extract(i).check()) { - index = boost::python::extract(i)(); - if (index < 0) - index += size(data.get()); - if (index >= (long)size(data.get()) || index < 0) { - PyErr_SetString(PyExc_IndexError, "Index out of range"); - boost::python::throw_error_already_set(); - } - } else { - PyErr_SetString(PyExc_TypeError, "Invalid index type"); - boost::python::throw_error_already_set(); - } - return boost::python::object(alps::alea::mcdata(data.get(), index)); - } - } - - template bool contains(alps::alea::mcdata & data, PyObject* key) { - boost::python::extract const &> x(key); - if (x.check()) - return std::find(data.begin(), data.end(), x()) != data.end(); - else { - boost::python::extract > x(key); - if (x.check()) - return std::find(data.begin(), data.end(), x()) != data.end(); - else - return false; - } - } - - #define ALPS_PY_MCDATA_WRAPPER(member_name) \ - template typename alps::alea::mcdata::result_type wrap_ ## member_name(alps::alea::mcdata const & value) { \ - return value. member_name (); \ - } \ - template alps::python::numpy::array wrap_ ## member_name(alps::alea::mcdata const & value) { \ - return alps::python::numpy::convert(value. member_name ()); \ - } - - ALPS_PY_MCDATA_WRAPPER(mean) - ALPS_PY_MCDATA_WRAPPER(error) - ALPS_PY_MCDATA_WRAPPER(tau) - ALPS_PY_MCDATA_WRAPPER(variance) - ALPS_PY_MCDATA_WRAPPER(bins) - ALPS_PY_MCDATA_WRAPPER(jackknife) - #undef ALPS_PY_MCDATA_WRAPPER - - template boost::python::str print_mcdata(alps::alea::mcdata const & self) { - return boost::python::str(boost::python::str(self.mean()) + " +/- " + boost::python::str(self.error())); - } - template boost::python::str format_mcdata(alps::alea::mcdata const & self, boost::python::str const & format_spec) { - #if PY_VERSION_HEX >= 0x03000000 - boost::python::object builtin = boost::python::import("builtins"); - #else - boost::python::object builtin = boost::python::import("__builtin__"); - #endif - boost::python::object globals(builtin.attr("__dict__")); - boost::python::object format_func = globals["format"]; - // return boost::python::str(boost::python::call(format_func.ptr(), self.mean(), format_spec)); - return boost::python::str(boost::python::str(boost::python::call(format_func.ptr(), self.mean(), format_spec)) + " +/- " + boost::python::str(boost::python::call(format_func.ptr(), self.error(), format_spec))); - // return boost::python::str(boost::python::format(self.mean(), format_spec) + " +/- " + boost::python::format(self.error(), format_spec)); - } - - template boost::python::str print_mcdata(alps::alea::mcdata > const & self) { - boost::python::str str; - for (typename alps::alea::mcdata >::const_iterator it = self.begin(); it != self.end(); ++it) - str += print_mcdata(*it) + (it + 1 != self.end() ? "\n" : ""); - return str; - } - - template boost::python::str format_mcdata(alps::alea::mcdata > const & self, boost::python::str const & format_spec) { - boost::python::str str; - for (typename alps::alea::mcdata >::const_iterator it = self.begin(); it != self.end(); ++it) - str += format_mcdata(*it, format_spec) + (it + 1 != self.end() ? "\n" : ""); - return str; - } - - } -} - -using namespace alps::alea; -using namespace boost::python; - -const char mcdata_docstring[] = -"This class is used to evaluate Monte Carlo data and functions on them. " -"Besides the documented functions and properties, the class supports the " -"arithmetic operations +, -, *, /, +=, -=, *=, /=, and the functions " -"abs, acos, acosh, asin, asinh, atan, atanh, cb, cbrt, cos, cosh, exp, log, " -"pow, sin, sinh, sq, sqrt, tan and tanh."; - - -const char init_docstring[] = -"Optionally the constructor takes one argument for the mean values, " -"and a second optional argumnt for the error."; - -const char save_docstring[] = -"Saves the object into the HDF5 file specified as the first string argument, " -"using as observable name the second string argument."; - -const char load_docstring[] = -"Loads the object into from HDF5 file specified as the first string argument, " -"using as observable name the second string argument."; - -const char mean_docstring[] = -"the mean value of all measurements recorded."; - -const char error_docstring[] = -"the error of all measurements recorded."; - -const char tau_docstring[] = -"the autocorrelation time estimate of the recorded measurements."; - -const char variance_docstring[] = -"the variance of all measurements recorded."; - -const char count_docstring[] = -"the number of measurements recorded."; - -const char bins_docstring[] = -"the bins recorded for a jackknife analysis."; - -const char jackknife_docstring[] = -"the jackknife data structure."; - - -BOOST_PYTHON_MODULE(pymcdata_c) { - - class_ >("MCScalarData", mcdata_docstring,init >(init_docstring)) - .add_property("mean", static_cast const &)>(&alps::python::wrap_mean),mean_docstring) - .add_property("error", static_cast const &)>(&alps::python::wrap_error),error_docstring) - .add_property("tau", static_cast const &)>(&alps::python::wrap_tau),tau_docstring) - .add_property("variance", static_cast const &)>(&alps::python::wrap_variance),variance_docstring) - .add_property("bins", static_cast const &)>(&alps::python::wrap_bins),bins_docstring) - .add_property("jackknife", static_cast const &)>(&alps::python::wrap_jackknife),jackknife_docstring) - .add_property("count", &mcdata::count,count_docstring) - .def("__repr__", static_cast const &)>(&alps::python::print_mcdata)) - .def("__format__", static_cast const &, str const &)>(&alps::python::format_mcdata)) - .def("__deepcopy__", &alps::python::make_copy >) - .def("__abs__", static_cast(*)(mcdata)>(&abs)) - .def("__pow__", static_cast(*)(mcdata, mcdata::element_type)>(&pow)) - .def(+self) - .def(-self) - .def(self += mcdata()) - .def(self += double()) - .def(self -= mcdata()) - .def(self -= double()) - .def(self *= mcdata()) - .def(self *= double()) - .def(self /= mcdata()) - .def(self /= double()) - .def(self + mcdata()) - .def(mcdata() + self) - .def(self + double()) - .def(double() + self) - .def(self - mcdata()) - .def(mcdata() - self) - .def(self - double()) - .def(double() - self) - .def(self * mcdata()) - .def(mcdata() * self) - .def(self * mcdata >()) - .def(mcdata >() * self) - .def(self * double()) - .def(double() * self) - .def(self / mcdata()) - .def(mcdata() / self) - .def(self / double()) - .def(double() / self) - .def("sq", static_cast(*)(mcdata)>(&sq)) - .def("cb", static_cast(*)(mcdata)>(&cb)) - .def("sqrt", static_cast(*)(mcdata)>(&sqrt)) - .def("cbrt", static_cast(*)(mcdata)>(&cbrt)) - .def("exp", static_cast(*)(mcdata)>(&exp)) - .def("log", static_cast(*)(mcdata)>(&log)) - .def("sin", static_cast(*)(mcdata)>(&sin)) - .def("cos", static_cast(*)(mcdata)>(&cos)) - .def("tan", static_cast(*)(mcdata)>(&tan)) - // .def("asin", static_cast(*)(mcdata)>(&asin)) - // .def("acos", static_cast(*)(mcdata)>(&acos)) - // .def("atan", static_cast(*)(mcdata)>(&atan)) - .def("sinh", static_cast(*)(mcdata)>(&sinh)) - .def("cosh", static_cast(*)(mcdata)>(&cosh)) - .def("tanh", static_cast(*)(mcdata)>(&tanh)) -// asinh, aconsh and atanh are not part of C++03 standard -// .def("asinh", static_cast(*)(mcdata)>(&asinh)) -// .def("acosh", static_cast(*)(mcdata)>(&acosh)) -// .def("atanh", static_cast(*)(mcdata)>(&atanh)) - .def("set_bin_size",&mcdata::set_bin_size) - .def("set_bin_number",&mcdata::set_bin_number) - .def("discard_bins",&mcdata::discard_bins) - .def("merge", static_cast::*)(mcdata const &)>(&mcdata::merge)) - .def("save", static_cast::*)(std::string const &, std::string const &) const>(&mcdata::save),save_docstring) - .def("load", static_cast::*)(std::string const &, std::string const &)>(&mcdata::load),load_docstring) - ; - - class_ > >("MCVectorData", mcdata_docstring, init >(init_docstring)) - .def("__len__", static_cast > &)>(&alps::python::size)) - .def("__getitem__", static_cast > & >, PyObject *)>(&alps::python::get_item)) - .def("__contains__", static_cast > &, PyObject *)>(&alps::python::contains)) - .add_property("mean", static_cast > const &)>(&alps::python::wrap_mean),mean_docstring) - .add_property("error", static_cast > const &)>(&alps::python::wrap_error),error_docstring) - .add_property("tau", static_cast > const &)>(&alps::python::wrap_tau),tau_docstring) - .add_property("variance", static_cast > const &)>(&alps::python::wrap_variance),variance_docstring) - .add_property("bins", static_cast > const &)>(&alps::python::wrap_bins),bins_docstring) - .add_property("jackknife", static_cast > const &)>(&alps::python::wrap_jackknife),jackknife_docstring) - .add_property("count", &mcdata >::count,count_docstring) - .def("__repr__", static_cast > const &)>(&alps::python::print_mcdata)) - .def("__format__", static_cast > const &, str const &)>(&alps::python::format_mcdata)) - .def("__deepcopy__", &alps::python::make_copy > >) - .def("__abs__", static_cast >(*)(mcdata >)>(&abs)) - .def("__pow__", static_cast >(*)(mcdata >, mcdata::element_type)>(&pow)) - .def(+self) - .def(-self) - .def(self == mcdata >()) - .def(self += mcdata >()) - .def(self += std::vector()) - .def(self -= mcdata >()) - .def(self -= std::vector()) - .def(self *= mcdata >()) - .def(self *= std::vector()) - .def(self /= mcdata >()) - .def(self /= std::vector()) - .def(self + mcdata >()) - .def(mcdata >() + self) - .def(self + std::vector()) - .def(std::vector() + self) - .def(self - mcdata >()) - .def(mcdata >() - self) - .def(self - std::vector()) - .def(std::vector() - self) - .def(self * mcdata >()) - .def(self * mcdata()) - .def(mcdata() * self) - .def(mcdata >() * self) - .def(self * std::vector()) - .def(std::vector() * self) - .def(self / mcdata >()) - .def(self / mcdata()) - .def(mcdata >() / self) - .def(self / std::vector()) - .def(std::vector() / self) - .def(self + double()) - .def(double() + self) - .def(self - double()) - .def(double() - self) - .def(self * double()) - .def(double() * self) - .def(self / double()) - .def(double() / self) - .def("sq", static_cast >(*)(mcdata >)>(&sq)) - .def("cb", static_cast >(*)(mcdata >)>(&cb)) - .def("sqrt", static_cast >(*)(mcdata >)>(&sqrt)) - .def("cbrt", static_cast >(*)(mcdata >)>(&cbrt)) - .def("exp", static_cast >(*)(mcdata >)>(&exp)) - .def("log", static_cast >(*)(mcdata >)>(&log)) - .def("sin", static_cast >(*)(mcdata >)>(&sin)) - .def("cos", static_cast >(*)(mcdata >)>(&cos)) - .def("tan", static_cast >(*)(mcdata >)>(&tan)) - // .def("asin", static_cast >(*)(mcdata >)>(&asin)) - // .def("acos", static_cast >(*)(mcdata >)>(&acos)) - // .def("atan", static_cast >(*)(mcdata >)>(&atan)) - .def("sinh", static_cast >(*)(mcdata >)>(&sinh)) - .def("cosh", static_cast >(*)(mcdata >)>(&cosh)) - .def("tanh", static_cast >(*)(mcdata >)>(&tanh)) -// asinh, aconsh and atanh are not part of C++03 standard -// .def("asinh", static_cast >(*)(mcdata >)>(&asinh)) -// .def("acosh", static_cast >(*)(mcdata >)>(&acosh)) -// .def("atanh", static_cast >(*)(mcdata >)>(&atanh)) - .def("set_bin_size",&mcdata >::set_bin_size) - .def("set_bin_number",&mcdata >::set_bin_number) - .def("discard_bins",&mcdata >::discard_bins) - .def("merge", static_cast >::*)(mcdata > const &)>(&mcdata >::merge)) - .def("save", static_cast >::*)(std::string const &, std::string const &) const>(&mcdata >::save),save_docstring) - .def("load", static_cast >::*)(std::string const &, std::string const &)>(&mcdata >::load),load_docstring) - ; -} diff --git a/src/alps/python/pytools.cpp b/src/alps/python/pytools.cpp deleted file mode 100644 index b6be4b3a0..000000000 --- a/src/alps/python/pytools.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 1994-2009 by Ping Nang Ma , -* Matthias Troyer , -* Bela Bauer -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -/* $Id: nobinning.h 3520 2009-12-11 16:49:53Z gamperl $ */ - - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -typedef boost::variate_generator > random_01; - -class WrappedRNG : public random_01 -{ -public: - WrappedRNG(int seed=0) - : random_01(boost::mt19937(seed), boost::uniform_01()) - { - } -}; - -const char convert2xml_docstring[] = - "converts a file to XML\n" - "\n" - "This function takes the path to an ALPS file as input and converts it to XML.\n" - "It returns a string with the path to the resulting XML file"; - -const char hdf5_name_encode_docstring[] = -"encodes a string for use in HDF5 paths\n" -"\n" -"This function takes a string and escapes all needed characters for it to be " -"used in HDF5 path names."; - -const char hdf5_name_decode_docstring[] = -"decodes a string fromHDF5 paths\n" -"\n" -"This function takes a string used in an HDF5 path name and replaces all " -"escaped characters."; - -const char search_xml_library_path_docstring[] = -"returns the full path for an ALPS XML file\n" -"\n" -"This function takes the name for an ALPS library XML or XSL file and returns " -"the full path."; - -const char rng_docstring[] = -"a uniform random number generator class\n\n" -"This class uses the Mersenne Twister rgenerator mt19937 to generate uniform " -"random numbers in the range [0,1).\n" -"The constructor takes an optional integer random seed argument.\n" -"Random numbers are created using the function call operator.\n"; - - -namespace { - void wrap_with_signature() - { - using namespace boost::python; - def("convert2xml", alps::convert2xml,convert2xml_docstring); - def("hdf5_name_encode", alps::hdf5_name_encode,hdf5_name_encode_docstring); - def("hdf5_name_decode", alps::hdf5_name_decode,hdf5_name_decode_docstring); - def("search_xml_library_path", alps::search_xml_library_path,search_xml_library_path_docstring); - /* - def("convert2numpy", - static_cast const& )> - (&convert2numpy)); - def("convert2numpy", - static_cast const& )> - (&convert2numpy)); - - def("convert2vector",&convert2vector); - def("convert2vector",&convert2vector); - */ - - } - - void wrap_without_signature() - { - using namespace boost::python; - docstring_options doc_options(true); - doc_options.disable_cpp_signatures(); - class_("rng", rng_docstring,init >("the constructor takes an optional integer argument as random number seed")) - .def("__deepcopy__", &alps::python::make_copy, "the deepcopy function creates a new copy of the generator") - .def("__call__", static_cast(&WrappedRNG::operator()), "returns a uniform random number in [0,1)") - ; - } - -} - -BOOST_PYTHON_MODULE(pytools_c) -{ - using namespace boost::python; - wrap_with_signature(); - wrap_without_signature(); -} - - diff --git a/src/alps/python/save_observable_to_hdf5.hpp b/src/alps/python/save_observable_to_hdf5.hpp deleted file mode 100644 index 89402e690..000000000 --- a/src/alps/python/save_observable_to_hdf5.hpp +++ /dev/null @@ -1,30 +0,0 @@ -/***************************************************************************** - * - * ALPS Project: Algorithms and Libraries for Physics Simulations - * - * ALPS Libraries - * - * Copyright (C) 2010 by Matthias Troyer , - * -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT - * - *****************************************************************************/ - -/* $Id: make_copy.hpp 4059 2010-03-29 08:36:25Z troyer $ */ - -#ifndef ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP -#define ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP - -#include - -namespace alps { namespace python { - - template void save_observable_to_hdf5(Obs const & obs, std::string const & filename) { - hdf5::archive ar(filename, "a"); - ar["/simulation/results/"+obs.representation()] << obs; - } - -} } // end namespace alps::python - -#endif // ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP diff --git a/src/boost/mpi/module.cpp b/src/boost/mpi/module.cpp deleted file mode 100644 index 57c2b5bbc..000000000 --- a/src/boost/mpi/module.cpp +++ /dev/null @@ -1,55 +0,0 @@ -// (C) Copyright 2006 Douglas Gregor - -// Use, modification and distribution is subject to the Boost Software -// License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) - -// Authors: Douglas Gregor - -/** @file module.cpp - * - * This file provides the top-level module for the Boost.MPI Python - * bindings. - */ -#include -#include - -using namespace boost::python; -using namespace boost::mpi; - -namespace boost { namespace mpi { namespace python { - -extern void export_environment(); -extern void export_exception(); -extern void export_collectives(); -extern void export_communicator(); -extern void export_datatypes(); -extern void export_request(); -extern void export_status(); -extern void export_timer(); -extern void export_nonblocking(); - -extern const char* module_docstring; - -BOOST_PYTHON_MODULE(mpi_c) -{ - // Setup module documentation - scope().attr("__doc__") = module_docstring; - scope().attr("__author__") = "Douglas Gregor "; - scope().attr("__date__") = "$LastChangedDate: 2008-06-26 12:25:44 -0700 (Thu, 26 Jun 2008) $"; - scope().attr("__version__") = "$Revision: 46743 $"; - scope().attr("__copyright__") = "Copyright (C) 2006 Douglas Gregor"; - scope().attr("__license__") = "http://www.boost.org/LICENSE_1_0.txt"; - - export_environment(); - export_exception(); - export_communicator(); - export_collectives(); - export_datatypes(); - export_request(); - export_status(); - export_timer(); - export_nonblocking(); -} - -} } } // end namespace boost::mpi::python diff --git a/tutorials/code-07-mcmain-mcbase/export.cpp b/tutorials/code-07-mcmain-mcbase/export.cpp deleted file mode 100644 index f2341db81..000000000 --- a/tutorials/code-07-mcmain-mcbase/export.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL isingsim_PyArrayHandle - -#include "ising.hpp" - -#include - -BOOST_PYTHON_MODULE(ising_c) { - ALPS_EXPORT_SIM_TO_PYTHON(sim, ising_sim); -} diff --git a/tutorials/code-07-mcmain-mcbase/export.py b/tutorials/code-07-mcmain-mcbase/export.py deleted file mode 100644 index 69fef1458..000000000 --- a/tutorials/code-07-mcmain-mcbase/export.py +++ /dev/null @@ -1,61 +0,0 @@ -from __future__ import print_function - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - # ALPS Project: Algorithms and Libraries for Physics Simulations # - # # - # ALPS Libraries # - # # - # Copyright (C) 2010 - 2013 by Lukas Gamper # - # # - # ALPS Project: https://alps.comp-phys.org/ # - # SPDX-License-Identifier: MIT # - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - -import pyalps.hdf5 as hdf5 -import pyalps.ngs as ngs -import sys, time, getopt - -import ising_c as ising - -if __name__ == '__main__': - - try: - optlist, positional = getopt.getopt(sys.argv[1:], 'T:c') - args = dict(optlist) - try: - limit = float(args['-T']) - except KeyError: - limit = 0 - resume = True if 'c' in args else False - outfile = positional[0] - except (IndexError, getopt.GetoptError): - print('usage: [-T timelimit] [-c] outputfile') - exit() - - sim = ising.sim(ngs.params({ - 'L': 100, - 'THERMALIZATION': 1000, - 'SWEEPS': 10000, - 'T': 2 - })) - - if resume: - try: - with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'r') as ar: - sim.load(ar['/']) - except ArchiveNotFound: pass - - if limit == 0: - sim.run(lambda: False) - else: - start = time.time() - sim.run(lambda: time.time() > start + float(limit)) - - with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'w') as ar: - ar['/'] = sim - - results = sim.collectResults() # TODO: how should we do that? - print(results) - - with hdf5.archive(outfile, 'w') as ar: # TODO: how sould we name archive? ngs.hdf5.archive? - ar['/parameters'] = sim.parameters - ar['/simulation/results'] = results diff --git a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/export.cpp b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/export.cpp deleted file mode 100644 index 700cd1340..000000000 --- a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/export.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#define PY_ARRAY_UNIQUE_SYMBOL ndsim_PyArrayHandle - -#include "ndim_spin.hpp" - -#include - -BOOST_PYTHON_MODULE(ndsim_c) { - ALPS_EXPORT_SIM_TO_PYTHON(xy_sim, ndim_spin_sim<2>); - ALPS_EXPORT_SIM_TO_PYTHON(heisenberg_sim, ndim_spin_sim<3>); - ALPS_EXPORT_SIM_TO_PYTHON(4d_sim, ndim_spin_sim<4>); - ALPS_EXPORT_SIM_TO_PYTHON(5d_sim, ndim_spin_sim<5>); -} diff --git a/tutorials/ngs/5_export_python/export2py.cpp b/tutorials/ngs/5_export_python/export2py.cpp deleted file mode 100644 index f2341db81..000000000 --- a/tutorials/ngs/5_export_python/export2py.cpp +++ /dev/null @@ -1,22 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2012 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#define PY_ARRAY_UNIQUE_SYMBOL isingsim_PyArrayHandle - -#include "ising.hpp" - -#include - -BOOST_PYTHON_MODULE(ising_c) { - ALPS_EXPORT_SIM_TO_PYTHON(sim, ising_sim); -} diff --git a/tutorials/ngs/5_export_python/ising.cpp b/tutorials/ngs/5_export_python/ising.cpp deleted file mode 100644 index 68e32a10a..000000000 --- a/tutorials/ngs/5_export_python/ising.cpp +++ /dev/null @@ -1,102 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2013 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "ising.hpp" - -#include - -ising_sim::ising_sim(parameters_type const & parms, std::size_t seed_offset) - : alps::mcbase(parms, seed_offset) - , length(parameters["L"]) - , sweeps(0) - , thermalization_sweeps(int(parameters["THERMALIZATION"])) - , total_sweeps(int(parameters["SWEEPS"])) - , beta(1. / double(parameters["T"])) - , spins(length) -{ - for(int i = 0; i < length; ++i) - spins[i] = (random() < 0.5 ? 1 : -1); - measurements - << alps::ngs::RealObservable("Energy") - << alps::ngs::RealObservable("Magnetization") - << alps::ngs::RealObservable("Magnetization^2") - << alps::ngs::RealObservable("Magnetization^4") - << alps::ngs::RealVectorObservable("Correlations") - ; -} - -void ising_sim::update() { - for (int j = 0; j < length; ++j) { - using std::exp; - int i = int(double(length) * random()); - int right = ( i + 1 < length ? i + 1 : 0 ); - int left = ( i - 1 < 0 ? length - 1 : i - 1 ); - double p = exp( 2. * beta * spins[i] * ( spins[right] + spins[left] )); - if ( p >= 1. || random() < p ) - spins[i] = -spins[i]; - } -} - -void ising_sim::measure() { - sweeps++; - if (sweeps > thermalization_sweeps) { - double tmag = 0; - double ten = 0; - double sign = 1; - std::vector corr(length); - for (int i = 0; i < length; ++i) { - tmag += spins[i]; - sign *= spins[i]; - ten += -spins[i] * spins[ i + 1 < length ? i + 1 : 0 ]; - for (int d = 0; d < length; ++d) - corr[d] += spins[i] * spins[( i + d ) % length ]; - } - std::transform(corr.begin(), corr.end(), corr.begin(), boost::lambda::_1 / double(length)); - ten /= length; - tmag /= length; - measurements["Energy"] << ten; - measurements["Magnetization"] << tmag; - measurements["Magnetization^2"] << tmag * tmag; - measurements["Magnetization^4"] << tmag * tmag * tmag * tmag; - measurements["Correlations"] << corr; - } -} - -double ising_sim::fraction_completed() const { - return (sweeps < thermalization_sweeps ? 0. : ( sweeps - thermalization_sweeps ) / double(total_sweeps)); -} - -void ising_sim::save(alps::hdf5::archive & ar) const { - mcbase::save(ar); - - std::string context = ar.get_context(); - ar.set_context("/simulation/realizations/0/clones/0/checkpoint"); - ar["sweeps"] << sweeps; - ar["spins"] << spins; - ar.set_context(context); -} - -void ising_sim::load(alps::hdf5::archive & ar) { - mcbase::load(ar); - - length = int(parameters["L"]); - thermalization_sweeps = int(parameters["THERMALIZATION"]); - total_sweeps = int(parameters["SWEEPS"]); - beta = 1. / double(parameters["T"]); - - std::string context = ar.get_context(); - ar.set_context("/simulation/realizations/0/clones/0/checkpoint"); - ar["sweeps"] >> sweeps; - ar["spins"] >> spins; - ar.set_context(context); -} diff --git a/tutorials/ngs/5_export_python/ising.hpp b/tutorials/ngs/5_export_python/ising.hpp deleted file mode 100644 index 6d90178cd..000000000 --- a/tutorials/ngs/5_export_python/ising.hpp +++ /dev/null @@ -1,51 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * * - * ALPS Project: Algorithms and Libraries for Physics Simulations * - * * - * ALPS Libraries * - * * - * Copyright (C) 2010 - 2013 by Lukas Gamper * - * * - * ALPS Project: https://alps.comp-phys.org/ * - * SPDX-License-Identifier: MIT * - * * - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef ALPS_TUTORIAL_ISING_HPP -#define ALPS_TUTORIAL_ISING_HPP - -#include - -#include -#include - -#include -#include - -class ALPS_DECL ising_sim : public alps::mcbase { - - public: - - ising_sim(parameters_type const & parms, std::size_t seed_offset = 0); - - virtual void update(); - virtual void measure(); - virtual double fraction_completed() const; - - using alps::mcbase::save; - virtual void save(alps::hdf5::archive & ar) const; - - using alps::mcbase::load; - virtual void load(alps::hdf5::archive & ar); - - private: - - int length; - int sweeps; - int thermalization_sweeps; - int total_sweeps; - double beta; - std::vector spins; -}; - -#endif diff --git a/tutorials/ngs/5_export_python/main.py b/tutorials/ngs/5_export_python/main.py deleted file mode 100644 index 71ae9ea1d..000000000 --- a/tutorials/ngs/5_export_python/main.py +++ /dev/null @@ -1,62 +0,0 @@ - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - # ALPS Project: Algorithms and Libraries for Physics Simulations # - # # - # ALPS Libraries # - # # - # Copyright (C) 2010 - 2013 by Lukas Gamper # - # # - # ALPS Project: https://alps.comp-phys.org/ # - # SPDX-License-Identifier: MIT # - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - -import pyalps.hdf5 as hdf5 -import pyalps.ngs as ngs -import numpy as np -import sys, time, getopt - -import ising_c as ising - -if __name__ == '__main__': - - try: - optlist, positional = getopt.getopt(sys.argv[1:], 'T:c') - args = dict(optlist) - try: - limit = float(args['-T']) - except KeyError: - limit = 0 - resume = True if 'c' in args else False - outfile = positional[0] - except (IndexError, getopt.GetoptError): - print 'usage: [-T timelimit] [-c] outputfile' - exit() - - sim = ising.sim(ngs.params({ - 'L': 100, - 'THERMALIZATION': 1000, - 'SWEEPS': 10000, - 'T': 2 - })) - - if resume: - try: - with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'r') as ar: - sim.load(ar['/']) - except ArchiveNotFound: pass - - if limit == 0: - sim.run(lambda: False) - else: - start = time.time() - sim.run(lambda: time.time() > start + float(limit)) - -# TODO: make this easier to understand - with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'w') as ar: - ar['/'] = sim - - results = sim.collectResults() # TODO: how should we do that? - print results - - with hdf5.archive(outfile, 'w') as ar: - ar['/parameters'] = sim.parameters - ar['/simulation/results'] = results From 0a46a3d3c7b0988f1df754d434535b1946adb8f5 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 16:10:27 -0500 Subject: [PATCH 19/52] docs(pyalps): record the free-threading and stable-ABI policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the two ABI non-decisions deliberate and visible (audit issues 21-23, 30-31, 42): the extension modules intentionally do not declare free-threading support — importing pyalps on 3.13t/3.14t re-enables the GIL, which is required while libalps uses the GIL as its lock around shared state (mcobservable's refcount table, the ngs::signal singleton, mcdata's lazy statistics) — and per-version wheels are kept instead of abi3, though the bindings are kept free of limited-API violations so stable-ABI builds remain an option. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/CMakeLists.txt | 4 ++++ bindings/python/pyalps/README.md | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index f006cb0b3..a45072382 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -56,6 +56,10 @@ set(_pyalps_targets pyngsrandom01_c pyngsaccumulator_c) +# Policy (see README "Free-threading and stable-ABI policy"): do NOT +# add FREE_THREADED (libalps relies on the GIL as its lock around +# shared state) and do not add STABLE_ABI without revisiting the wheel +# matrix — per-version wheels are deliberate. nanobind_add_module(pyalea_c NB_STATIC "${_bindings}/pyalea.cpp") nanobind_add_module(pymcdata_c NB_STATIC "${_bindings}/pymcdata.cpp") nanobind_add_module(pytools_c NB_STATIC "${_bindings}/pytools.cpp") diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 26b045750..ced5199a3 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -34,3 +34,24 @@ speed up rebuilds. `PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, DWA, CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake configuration for a smaller core-only developer build. + +## Free-threading and stable-ABI policy + +pyalps ships per-version wheels (CPython 3.10–3.14) and deliberately opts +into neither of nanobind's special ABI modes: + +- **Free-threading (3.13t/3.14t):** the extension modules do not declare + free-threading support, so importing pyalps on a free-threaded + interpreter re-enables the GIL for the process. That is intentional: + the ALPS C++ library relies on the GIL as its lock around shared state + (`mcobservable`'s reference-count table, the `alps::ngs::signal` + singleton, `mcdata`'s lazily-computed statistics). Do not add + `FREE_THREADED` to `nanobind_add_module` without first making that + state thread-safe. +- **Stable ABI (abi3):** the bindings contain no limited-API violations + (the last one, a `PyTuple_SET_ITEM`, was removed deliberately to keep + this option open), but per-version wheels are kept because the wheel + matrix is fully automated, linked abi3 would raise the floor to + CPython 3.12, and split mode adds a runtime dependency plus per-call + overhead on hot accessor paths. Revisit when a new CPython release + makes day-one support pressing. From c065b9bb5dc7f64e2abcc68c03e57c67b63d3698 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 17:00:47 -0500 Subject: [PATCH 20/52] fix(pyalps): apply pre-push audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ten-angle review plus empirical edge testing of the audit-response commits surfaced real gaps, all fixed and regression-tested: - Group-saved lists with 11+ elements loaded back as dicts (or out of order): H5Literate yields child names lexicographically, but the loader compared them positionally against '0','1',... Recover list shape from the name SET {0..n-1} and load in numeric order, as the legacy loader effectively did by indexing value[cast(name)]. - Lists of numpy scalars (np.int64, np.float32, np.bool_, ...) lost their legacy vectorization into one typed dataset, and rectangular ndarray/sequence mixes no longer stacked. numpy_stackable() now delegates both shapes through numpy.asarray — vetoing trees that contain plain bool leaves, which numpy would silently promote to 0/1 (the legacy rules always grouped those). - Re-saving a group-shaped list or dict over an existing group kept stale children (create_group is a no-op on an existing group); legacy wiped the group first. Both branches now delete it. - The params ladder silently widened out-of-int32 integers inside lists to double (corrupting values beyond 2^53) while raising for scalars, missed numpy bool scalars in its bool guard (stored as 1.0/0.0), and rejected numpy integer scalars while accepting numpy floats. One pre-scan now range-checks every integral element (PyNumber_Index covers numpy ints), numpy bools count as bools, and numpy integer scalars are accepted consistently as scalars and in lists. - observables lacked __delitem__, so the copied MutableMapping pop/popitem/clear raised TypeError; the legacy map_indexing_suite provided deletion. Bound it (mcobservables derives from std::map). - numpy_module()'s magic static could deadlock two GIL-juggling first callers; replaced with an atomic double-check. NULL results of PyTuple_New/PyLong_FromUnsignedLongLong are now checked. - The bindings' Boost config defines are now lifted from the SDK-exported ALPS_CMAKE_CXX_FLAGS instead of hand-mirrored. - Smaller items: params_getitem regained its defined() fast path for misses, list_vectorizer dropped derivable state and uses exact numeric type checks (numpy scalar handling moved to the stacking path), orphaned includes removed from mcbase.cpp, guard-collapse blank-line scars squeezed, mcdata assertions tightened to assert_allclose. Validated: wheel rebuild against the SDK, 23/23 Python tests. Co-Authored-By: Claude Fable 5 --- bindings/python/pyalps/CMakeLists.txt | 25 ++- bindings/python/pyalps/cpp/dict_to_params.hpp | 97 +++++++--- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 180 +++++++++++++----- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 4 - .../python/pyalps/cpp/ngs/observables.cpp | 8 + bindings/python/pyalps/cpp/ngs/params.cpp | 11 +- bindings/python/pyalps/cpp/numpy_compat.hpp | 37 +++- parms1.h5 | Bin 0 -> 6176 bytes parms2.h5 | Bin 0 -> 6176 bytes py.h5 | Bin 0 -> 7352 bytes src/alps/alea/mcanalyze.hpp | 2 - src/alps/alea/mcdata.hpp | 2 - src/alps/alea/value_with_error.hpp | 1 - src/alps/ngs/detail/paramvalue.hpp | 1 - src/alps/ngs/detail/paramvalue_reader.hpp | 5 - src/alps/ngs/lib/params.cpp | 1 - src/alps/ngs/lib/paramvalue.cpp | 3 - src/alps/ngs/params.hpp | 2 - src/alps/ngs/scheduler/proto/mcbase.hpp | 3 - test/pyalps/mcdata_test.py | 4 +- test/pyalps/pyhdf5io_test.py | 52 ++++- test/pyalps/test_binding_surface.py | 43 ++++- 22 files changed, 362 insertions(+), 119 deletions(-) create mode 100644 parms1.h5 create mode 100644 parms2.h5 create mode 100644 py.h5 diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index a45072382..4faf3af85 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -127,15 +127,24 @@ if(PYALPS_BUILD_APPLICATIONS) target_include_directories(dwa_c PRIVATE "${_alps_source_root}/applications/qmc/dwa") endif() +# Compile the bindings with the same preprocessor configuration the +# ALPS SDK was built with (BOOST_NO_AUTO_PTR and friends), so the Boost +# headers on both sides of the library boundary are configured +# identically. The -D entries are lifted from the exported +# ALPS_CMAKE_CXX_FLAGS rather than hand-copied from the root +# CMakeLists, so a define added there cannot silently desync. +separate_arguments(_alps_sdk_cxx_flags NATIVE_COMMAND "${ALPS_CMAKE_CXX_FLAGS}") +set(_alps_sdk_definitions "") +foreach(_flag IN LISTS _alps_sdk_cxx_flags) + if(_flag MATCHES "^-D(.+)$") + list(APPEND _alps_sdk_definitions "${CMAKE_MATCH_1}") + endif() +endforeach() + foreach(_target IN LISTS _pyalps_targets) - # Mirror the Boost configuration macros libalps is compiled with - # (root CMakeLists.txt, CMAKE_CXX_FLAGS) so the Boost headers both - # sides of the ALPS library boundary include are configured - # identically. - target_compile_definitions(${_target} PRIVATE - BOOST_NO_AUTO_PTR - BOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF - BOOST_TIMER_ENABLE_DEPRECATED) + if(_alps_sdk_definitions) + target_compile_definitions(${_target} PRIVATE ${_alps_sdk_definitions}) + endif() target_include_directories(${_target} PRIVATE ${ALPS_INCLUDE_DIRS} ${ALPS_EXTRA_INCLUDE_DIRS}) diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index b3b7bc20e..c4e197129 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -14,23 +14,42 @@ #include #include #include +#include +#include #include #include namespace pyalps { namespace nb = nanobind; +namespace detail { +inline bool is_bool_like(PyObject * raw) { + // plain bool, or a numpy bool scalar (numpy.bool_ / numpy.bool), + // which does NOT subclass bool and would otherwise slip through + // the numeric ladder as 0.0/1.0 + return PyBool_Check(raw) + || std::strncmp(Py_TYPE(raw)->tp_name, "numpy.bool", 10) == 0; +} +} // namespace detail // Store one Python value under `key`. paramvalue's only integral // alternative is a 32-bit int and libalps static_casts wider integer -// types down to it, so out-of-range Python ints are rejected loudly -// here rather than truncated silently. List probes use exact element -// types first (convert=false) so integer lists round-trip as ints; -// mixed numeric lists without bools widen to double. +// types down to it, so out-of-range integers are rejected loudly here +// — for scalars and inside lists alike — rather than truncated or +// silently widened to double. List probes use exact element types +// first (convert=false) so integer lists round-trip as ints; mixed +// numeric lists without bools widen to double. inline void set_param_value(alps::params & p, std::string const & key, nb::handle value) { if (value.is_none()) throw nb::type_error(("cannot store None for parameter '" + key + "': params has no null type; delete the key instead").c_str()); - if (nb::isinstance(value)) { - p[key] = nb::cast(value); - } else if (nb::isinstance(value)) { + if (detail::is_bool_like(value.ptr())) { + // PyObject_IsTrue rather than nb::cast: the caster does + // not convert numpy bool scalars + int const truth = PyObject_IsTrue(value.ptr()); + if (truth < 0) + throw nb::python_error(); + p[key] = (truth == 1); + } else if (nb::isinstance(value) || PyIndex_Check(value.ptr())) { + // PyIndex_Check admits numpy integer scalars (np.int64 etc.), + // which don't subclass int the way np.float64 subclasses float try { p[key] = nb::cast(value); } catch (nb::cast_error const &) { @@ -44,31 +63,63 @@ inline void set_param_value(alps::params & p, std::string const & key, nb::handl } else if (nb::isinstance(value)) { p[key] = nb::cast(value); } else if (nb::isinstance(value) || nb::isinstance(value)) { - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - // mixed numeric content (e.g. [1, 2.5]) widens to double — - // but never bools, which would silently become 0.0/1.0 - nb::object seq = nb::borrow(value); - std::size_t const n = nb::len(seq); + // One pre-scan enforcing the loud-failure policies explicitly, + // independent of caster conversion behaviour: bools never + // coerce to numbers, and oversized integers raise exactly like + // the scalar arm instead of widening to double (which would + // corrupt values beyond 2^53). + std::size_t const length = nb::len(value); bool has_bool = false; - for (std::size_t i = 0; i < n && !has_bool; ++i) { - nb::object item = seq[i]; - has_bool = nb::isinstance(item); + for (std::size_t i = 0; i < length; ++i) { + nb::object item = value[i]; + PyObject * raw = item.ptr(); + if (detail::is_bool_like(raw)) { + has_bool = true; + } else if (PyLong_Check(raw) || PyIndex_Check(raw)) { + // PyNumber_Index covers numpy integer scalars too — + // they are not PyLong subclasses but must obey the + // same 32-bit range policy + PyObject * as_long = PyNumber_Index(raw); + if (!as_long) { + PyErr_Clear(); + continue; + } + int overflow = 0; + long long v = PyLong_AsLongLongAndOverflow(as_long, &overflow); + Py_DECREF(as_long); + if (overflow + || v < std::numeric_limits::min() + || v > std::numeric_limits::max()) + throw nb::type_error(("parameter '" + key + + "' contains an integer that does not fit params'" + " 32-bit integer type").c_str()); + } } if (!has_bool) { + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>>(value, false); return; } + catch (nb::cast_error const &) {} + try { p[key] = nb::cast>(value, false); return; } + catch (nb::cast_error const &) {} + // numpy integer scalars satisfy the convert=true int + // caster via __index__ (floats don't), keeping + // [np.int64(8)] consistent with the scalar np.int64 rung; + // the pre-scan above already range-checked every element + try { p[key] = nb::cast>(value); return; } + catch (nb::cast_error const &) {} + // mixed numeric content (e.g. [1, 2.5] or numpy floats) + // widens to double / complex try { p[key] = nb::cast>(value); return; } catch (nb::cast_error const &) {} try { p[key] = nb::cast>>(value); return; } catch (nb::cast_error const &) {} } throw nb::type_error(("unsupported list for parameter '" + key - + "' (expected homogeneous numbers or strings)").c_str()); + + "' (expected homogeneous numbers or strings; bools are not" + " a parameter list type)").c_str()); } else { throw nb::type_error(("unsupported type for parameter '" + key + "' (expected bool/int/float/complex/str or a list of those)").c_str()); diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 128e7de23..7d6729b2a 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -45,44 +47,47 @@ namespace alps { struct list_vectorizer { enum class leaf_kind { none, integral, floating, cplx, text }; std::vector extent; // rectangular extents per depth - std::ptrdiff_t leaf_depth = -1; - leaf_kind kind = leaf_kind::none; + leaf_kind kind = leaf_kind::none; // != none also means "a leaf was seen" std::vector ints; std::vector reals; std::vector> cplxs; std::vector texts; bool fits_int = true; bool analyze(nb::handle node, std::size_t depth) { - nb::object seq = nb::borrow(node); - std::size_t const n = nb::len(seq); + std::size_t const n = nb::len(node); if (depth == extent.size()) extent.push_back(n); else if (extent[depth] != n) return false; // ragged for (std::size_t i = 0; i < n; ++i) { - nb::object item = seq[i]; - PyObject * p = item.ptr(); - if (PyBool_Check(p)) + nb::object item = node[i]; + PyObject * raw = item.ptr(); + if (PyBool_Check(raw)) return false; // legacy: bool never vectorizes - if (PyList_Check(p) || PyTuple_Check(p)) { - // a sequence may not appear at the leaf level - if (leaf_depth != -1 - && static_cast(depth + 1) >= leaf_depth) + if (PyList_Check(raw) || PyTuple_Check(raw)) { + // once a leaf has fixed the depth (extent can no + // longer grow), sequences may not appear at or + // below the leaf level + if (kind != leaf_kind::none && depth + 1 >= extent.size()) return false; if (!analyze(item, depth + 1)) return false; continue; } - // scalar leaf: all leaves must sit at one depth - if (leaf_depth == -1) { - if (extent.size() != depth + 1) - return false; - leaf_depth = static_cast(depth + 1); - } else if (leaf_depth != static_cast(depth + 1)) + // scalar leaf: all leaves sit at one depth — the + // deepest extent recorded so far + if (depth + 1 != extent.size()) return false; - if (PyLong_Check(p)) { + // Exact numeric types only, mirroring the legacy + // tp_name dispatch: numpy scalars (np.float64 and + // np.complex128 included, although they subclass the + // builtins) take the numpy-stacking path below, + // which preserves their dtype like the legacy + // scalar_types table did. str accepts subclasses — + // np.str_ was a legacy string dtype too. + if (PyLong_Check(raw)) { // np ints don't subclass int int overflow = 0; - long long v = PyLong_AsLongLongAndOverflow(p, &overflow); + long long v = PyLong_AsLongLongAndOverflow(raw, &overflow); if (overflow) return false; // → descent; the per-element save raises, like legacy if (!accept(leaf_kind::integral)) @@ -90,18 +95,16 @@ namespace alps { if (v < std::numeric_limits::min() || v > std::numeric_limits::max()) fits_int = false; ints.push_back(v); - } else if (PyFloat_Check(p)) { - // includes numpy.float64, which subclasses float + } else if (PyFloat_CheckExact(raw)) { if (!accept(leaf_kind::floating)) return false; - reals.push_back(PyFloat_AsDouble(p)); - } else if (PyComplex_Check(p)) { - // includes numpy.complex128, which subclasses complex + reals.push_back(PyFloat_AsDouble(raw)); + } else if (PyComplex_CheckExact(raw)) { if (!accept(leaf_kind::cplx)) return false; - Py_complex c = PyComplex_AsCComplex(p); + Py_complex c = PyComplex_AsCComplex(raw); cplxs.emplace_back(c.real, c.imag); - } else if (PyUnicode_Check(p)) { + } else if (PyUnicode_Check(raw)) { if (!accept(leaf_kind::text)) return false; texts.push_back(nb::cast(item)); @@ -171,13 +174,16 @@ namespace alps { case list_vectorizer::leaf_kind::none: break; // e.g. [[], []] → group descent } - } else if (all_ndarrays(l)) { - // Legacy stacked equal-shape numpy arrays into one - // dataset; delegate to numpy so shape checking and - // dtype promotion match numpy's rules, then feed + } else if (numpy_stackable(l)) { + // Legacy vectorized numpy content too: homogeneous + // numpy-scalar lists (numpy.int64 etc. were + // scalar_types entries) and rectangular trees + // mixing ndarrays with nested sequences all became + // one dataset. Delegate to numpy so shape checking + // and dtype handling match numpy's rules, then feed // the stacked array through the ndarray save path. - // Ragged shapes (numpy raises) and object dtype - // fall through to the group descent below. + // Ragged shapes (numpy raises) and non-numeric + // dtypes fall through to the group descent below. nb::object arr; try { arr = nb::borrow(alps::python::numpy_module()) @@ -186,10 +192,10 @@ namespace alps { arr = nb::object(); } if (arr.is_valid()) { - std::string dtype_kind = + std::string const dtype_kind = nb::cast(arr.attr("dtype").attr("kind")); - if (dtype_kind.find_first_of("biufc") != std::string::npos - && dtype_kind.size() == 1) { + if (dtype_kind.size() == 1 + && std::strchr("biufc", dtype_kind[0])) { hdf5_save_py11_visitor child_visitor{ar, path}; extract_from_pyobject_py11(child_visitor, arr); return; @@ -199,6 +205,10 @@ namespace alps { // Heterogeneous / ragged / bool-containing — recurse // per-element into /, letting each entry // be stored as its own native type (legacy behaviour). + // Legacy wiped any existing group before a list save; + // create_group alone would keep stale children around. + if (ar.is_group(path)) + ar.delete_group(path); ar.create_group(path); Py_ssize_t i = 0; for (auto item : l) { @@ -207,16 +217,73 @@ namespace alps { extract_from_pyobject_py11(child_visitor, item); } } - static bool all_ndarrays(nb::list const & l) { - for (auto item : l) - if (std::string(item.ptr()->ob_type->tp_name) != "numpy.ndarray") + static bool is_ndarray(PyObject * raw) { + return std::strcmp(Py_TYPE(raw)->tp_name, "numpy.ndarray") == 0; + } + struct tree_scan { + bool has_ndarray = false; + bool has_bool_leaf = false; + }; + static void scan_tree(nb::handle node, tree_scan & scan) { + std::size_t const n = nb::len(node); + for (std::size_t i = 0; i < n; ++i) { + nb::object item = node[i]; + PyObject * raw = item.ptr(); + if (is_ndarray(raw)) + scan.has_ndarray = true; + else if (PyList_Check(raw) || PyTuple_Check(raw)) + scan_tree(item, scan); + else if (PyBool_Check(raw) + || std::strncmp(Py_TYPE(raw)->tp_name, "numpy.bool", 10) == 0) + scan.has_bool_leaf = true; + if (scan.has_bool_leaf) + return; // verdict fixed: bool leaves veto stacking + } + } + // The list shapes the legacy build stacked into one + // dataset beyond plain scalars: (a) numpy scalars of ONE + // type (exact tp_name match, like legacy scalar_types), or + // (b) sequences/ndarrays only, with an ndarray somewhere in + // the tree (legacy vectorized extent-matched mixes of + // list/tuple/ndarray nodes) — but never when a plain bool + // sits among the leaves, which numpy would silently promote + // to 0/1. Pure-list trees never reach (b) — their + // exact-type handling stays with list_vectorizer. + static bool numpy_stackable(nb::list const & l) { + char const * first_scalar = nullptr; + bool scalars_only = true; + bool sequences_only = true; + for (auto item : l) { + PyObject * raw = item.ptr(); + char const * tp = Py_TYPE(raw)->tp_name; + if (is_ndarray(raw) || PyList_Check(raw) || PyTuple_Check(raw)) { + scalars_only = false; + continue; + } + sequences_only = false; + if (std::strncmp(tp, "numpy.", 6) != 0) return false; - return true; + if (!first_scalar) + first_scalar = tp; + else if (std::strcmp(tp, first_scalar) != 0) + return false; + } + if (scalars_only && first_scalar) + return true; + if (!sequences_only) + return false; + tree_scan scan; + scan_tree(l, scan); + return scan.has_ndarray && !scan.has_bool_leaf; } void operator()(nb::dict const & d) const { // Store a dict as a group with one child per key. Keys // are stringified (HDF5 paths are strings), values go - // through the same save dispatch recursively. + // through the same save dispatch recursively. Like the + // list descent above (and the legacy build), wipe an + // existing group first so stale keys don't survive. + if (ar.is_group(path)) + ar.delete_group(path); ar.create_group(path); for (auto item : d) { std::string key = nb::cast(nb::str(item.first)); @@ -266,21 +333,40 @@ namespace alps { nb::object python_hdf5_load_impl(alps::hdf5::archive & ar, std::string const & path) { // Groups (not datasets) get loaded recursively. Children - // whose names are consecutive decimal integers starting at 0 - // are recovered as a Python list (preserving round-trip for - // list-saved-as-group); otherwise a dict. + // whose names are exactly the decimal integers 0..n-1 are + // recovered as a Python list (preserving round-trip for + // list-saved-as-group); otherwise a dict. The backend + // yields child names in lexicographic order ("0", "1", + // "10", "2", ...), so the check is on the name SET and the + // list is loaded in numeric order — the legacy loader was + // order-insensitive the same way, indexing + // value[cast(name)]. if (ar.is_group(path)) { auto children = ar.list_children(path); bool list_shaped = true; - for (std::size_t i = 0; list_shaped && i < children.size(); ++i) { - if (children[i] != std::to_string(i)) + std::vector seen(children.size(), false); + for (auto const & child : children) { + bool numeric = !child.empty() && child.size() < 20; + for (char c : child) + if (c < '0' || c > '9') { + numeric = false; + break; + } + std::size_t index = numeric + ? static_cast(std::strtoull(child.c_str(), nullptr, 10)) + : 0; + if (!numeric || std::to_string(index) != child + || index >= children.size() || seen[index]) { list_shaped = false; + break; + } + seen[index] = true; } if (list_shaped) { nb::list result; - for (auto const & child : children) + for (std::size_t i = 0; i < children.size(); ++i) result.append( - python_hdf5_load_impl(ar, path + "/" + child)); + python_hdf5_load_impl(ar, path + "/" + std::to_string(i))); return nb::object(std::move(result)); } else { nb::dict result; diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index a842a5c1b..e87a90266 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -44,8 +44,6 @@ #include #include #include -#include -#include #include namespace nb = nanobind; #ifdef ALPS_HAVE_MPI @@ -54,8 +52,6 @@ namespace nb = nanobind; #include #include #include -#include -#include #include "../dict_to_params.hpp" namespace alps { // Trampoline: holds Python overrides for pure-virtuals. The diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index 1813112b6..e6d30d9c7 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -85,6 +85,14 @@ NB_MODULE(pyngsobservables_c, m) { .def("__setitem__", [](alps::mcobservables & self, std::string const & k, alps::mcobservable const & v) { self.insert(k, v); }) + // mcobservables derives publicly from std::map; item deletion + // restores what the legacy map_indexing_suite provided (and + // what the MutableMapping mixins pop/popitem/clear need). + .def("__delitem__", [](alps::mcobservables & self, std::string const & k) { + if (!self.has(k)) + throw nb::key_error(k.c_str()); + self.erase(k); + }) .def("__iter__", [](alps::mcobservables & self) { return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); }, diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index 93238e513..02df54e0f 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -44,13 +44,18 @@ void params_setitem(alps::params & self, nb::object const & key_obj, nb::object } nb::object params_getitem(alps::params & self, nb::object const & key_obj) { std::string key = nb::cast(nb::str(key_obj)); + // defined() answers the (common) miss with one map lookup; + // paramiterator steps re-do a map find each, so walking the whole + // container to conclude "absent" would be much slower. + if (!self.defined(key)) + return nb::none(); // params doesn't expose the underlying map directly, but - // paramiterator yields (key, paramvalue) pairs; a single walk both - // answers "defined?" and hands the variant to paramvalue_to_py. + // paramiterator yields (key, paramvalue) pairs; walk it to find the + // entry and hand the variant to paramvalue_to_py. for (auto it = self.begin(); it != self.end(); ++it) if (it->first == key) return paramvalue_to_py(it->second); - return nb::none(); + return nb::none(); // defensive — defined()==true should guarantee a hit } void params_delitem(alps::params & self, nb::object const & key_obj) { self.erase(nb::cast(nb::str(key_obj))); diff --git a/bindings/python/pyalps/cpp/numpy_compat.hpp b/bindings/python/pyalps/cpp/numpy_compat.hpp index 187b86781..abcff3c61 100644 --- a/bindings/python/pyalps/cpp/numpy_compat.hpp +++ b/bindings/python/pyalps/cpp/numpy_compat.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -40,11 +41,26 @@ namespace alps { template <> struct numpy_dtype> { static constexpr char const* name = "complex128"; }; // Cached numpy module. Importing per call was a sys.modules // lookup + import-lock acquisition on every array conversion. - // The reference is deliberately leaked: a static nb_::object - // would decref during static destruction, potentially after - // interpreter finalization. + // Not a function-local static: the winning thread's import can + // release the GIL, so blocking a GIL-holding second thread on + // the C++ static-init guard would deadlock. With the atomic + // double-check, racing first callers both import (idempotent + // under the import lock) and the loser drops its reference. + // The winning reference is deliberately leaked so it stays + // valid until interpreter shutdown regardless of static + // destruction order. inline nb_::handle numpy_module() { - static PyObject * mod = nb_::module_::import_("numpy").release().ptr(); + static std::atomic cached{nullptr}; + PyObject * mod = cached.load(std::memory_order_acquire); + if (!mod) { + mod = nb_::module_::import_("numpy").release().ptr(); + PyObject * expected = nullptr; + if (!cached.compare_exchange_strong(expected, mod, + std::memory_order_acq_rel)) { + Py_DECREF(mod); + mod = expected; + } + } return mod; } // Allocates numpy.empty(shape, dtype=numpy_dtype::name) and @@ -55,13 +71,18 @@ namespace alps { std::vector const& shape) { nb_::handle np = numpy_module(); nb_::tuple shape_tuple = nb_::steal(PyTuple_New(static_cast(shape.size()))); + if (!shape_tuple.is_valid()) + throw nb_::python_error(); // PyTuple_SetItem (not the SET_ITEM macro): the macro pokes // tuple internals directly and is unavailable under the // limited API, which is otherwise within reach for these - // bindings. - for (std::size_t i = 0; i < shape.size(); ++i) - PyTuple_SetItem(shape_tuple.ptr(), static_cast(i), - PyLong_FromUnsignedLongLong(shape[i])); + // bindings. SetItem steals the reference to dim. + for (std::size_t i = 0; i < shape.size(); ++i) { + PyObject * dim = PyLong_FromUnsignedLongLong(shape[i]); + if (!dim) + throw nb_::python_error(); + PyTuple_SetItem(shape_tuple.ptr(), static_cast(i), dim); + } nb_::object arr = np.attr("empty")( shape_tuple, nb_::arg("dtype") = numpy_dtype::name); // Bridge the freshly-allocated numpy buffer through nb::ndarray diff --git a/parms1.h5 b/parms1.h5 new file mode 100644 index 0000000000000000000000000000000000000000..a3af53cf4ba9ea14abcde8afa8dacb4be44887a3 GIT binary patch literal 6176 zcmeD5aB<`1lHy|G;9!7(|4^VH0TD5PN=(a2)$#Xm31ZUucd;c48E;`y0;^$Wg(`&^ zflhNVF))IZu!)06MivkW0xV^TIfnlsY`7#?B~;Q#nh7GyCIX@vS)g17#zb>wR2e9j zfw3YOYQBH~NJTdrL>1%ROgF-(Dh73>FQ zE)F4(dCZIq(2#?BW*@|!p-iexQWGU2Y(zj|BMAy0pl3lC5@j5)Fk*s*5eqboz!3oo zm~9|pXo8igSJx5|HU!g+2sC>Qr*t#rl1>se!bV~khRxqJ&X?2(8<}AkHrF4nboY1m z@P?J{usp$-ln<>^q9vlehh}*(s9miEbRDRg6M+`cpaR_8+203TkV8vrNTCi<2l9-G z5r{;98~h0NC~GtXMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU}%H@sJ{;$ H`UcVfD$$NH literal 0 HcmV?d00001 diff --git a/parms2.h5 b/parms2.h5 new file mode 100644 index 0000000000000000000000000000000000000000..d879fd8a56e42e800509220c796aa68bcf9ac4df GIT binary patch literal 6176 zcmeD5aB<`1lHy|G;9!7(|4^VH0TD5PN=(a2)$#Xm31ZUucd;c48E;`y0;^$Wgerv@ zflhNVF@Tk@$$}IzvOtwHa1|sLCFZ7U7|JFN(F5f&u#_d{7_vb_1SZK0Q45wdQi3{1Kma5f z#SW2!gggU-op;_Tuy=HkV+O=#V1&2~WD_$FC;|k4kqxm)fq@kqCCpqLLLl>iF$hfv z@VMCrv1cfgYLnDNsS!3T!!T^}8xMS^M%cg#kl~bW^q4Q$5fL^bpgbiB%2&Wl0>Y5o z&H>9?Ot8Gg15H2R3=GOp+d#z71kD16{t*#21j`#4Xe~3G(#`dUE8YE_J-lJ1Kg@58 z6?SZp*#2>7TmE2(cTh?NRWe#Y45}YwpeYn2?(XdG15U}%Q5bVSny#f?BB2o|&r6kV6xz1NJt2Rh@hvidW#~od+t4FTE;PrL|C}9?0ommxj(--=L~%JcBZGd zD%p^9To?FSqw87PJUL2+mHGZ(*-X!A=kV&%&9Uxz7;Hk5V{XeAyc?wPqAZ$X@naUdyxnB!?+fhMS6XuI21vjuLd zk(+(ZokW2T?R%hoO_3kwC`KFwPWH5N$?2LIsx{nvSvFz7=m}Ac`*MU3?4FErGp^#U z?khouh8pD?hlO90*3PF~EgVl=PqpE^5>o1?{4kdrnVcw&=cjWy?YbtET)@Q$xPWbU z<{F)SaDA(ORF83{mW6*_F!_5p{5OuZE}qMFXZoYovPPquwEvR6671jFm}ohd zvZxC3It}UFsSOzQumI2FvWxz6E8L$jg0t1_fQ!EFBMJcZTO`Q77I>hkK5b^aV ze2-R_GD5WC8XP6su2Yw2al!N8EMT45G=NPdWHwZr_;i5P7*gkePfSR60Tg8PHP3}g z!80pW_o6xWg=oiJIC8x%&Z^u-xCPz32gjD+BdCjYbH>2nYcseZ^r-G_x6a;c=G%_@ zQs(TS2}eF_U<{7lcp47BFmgD;Rc;xH(T8s{P1Lw6veUmleDUVvg4T*_u&0P|a5#nH XH42afnK(|IiOX}1W}bt`PYwMBBN~h} literal 0 HcmV?d00001 diff --git a/src/alps/alea/mcanalyze.hpp b/src/alps/alea/mcanalyze.hpp index 1fab6d88e..6068a1c00 100644 --- a/src/alps/alea/mcanalyze.hpp +++ b/src/alps/alea/mcanalyze.hpp @@ -31,8 +31,6 @@ #include #include - - #include #include #include diff --git a/src/alps/alea/mcdata.hpp b/src/alps/alea/mcdata.hpp index 8a56bf2ef..1b8b1f479 100644 --- a/src/alps/alea/mcdata.hpp +++ b/src/alps/alea/mcdata.hpp @@ -60,7 +60,6 @@ #include #include - namespace alps { namespace alea { @@ -153,7 +152,6 @@ namespace alps { , error_(error) {} - std::size_t size() const { return bins().size();} template mcdata(mcdata const & rhs, S s) diff --git a/src/alps/alea/value_with_error.hpp b/src/alps/alea/value_with_error.hpp index 65eff12e6..a379cf1b0 100644 --- a/src/alps/alea/value_with_error.hpp +++ b/src/alps/alea/value_with_error.hpp @@ -19,7 +19,6 @@ #include #include - #include #include #include diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index 624349b5f..8fef1a643 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -19,7 +19,6 @@ #include #include - #include #include #include diff --git a/src/alps/ngs/detail/paramvalue_reader.hpp b/src/alps/ngs/detail/paramvalue_reader.hpp index f0db912ed..eb93bf5a1 100644 --- a/src/alps/ngs/detail/paramvalue_reader.hpp +++ b/src/alps/ngs/detail/paramvalue_reader.hpp @@ -19,7 +19,6 @@ #include - namespace alps { namespace detail { @@ -33,7 +32,6 @@ namespace alps { throw std::runtime_error(std::string("cannot cast from std::vector<") + typeid(U).name() + "> to " + typeid(T).name() + ALPS_STACKTRACE); } - T value; }; @@ -51,7 +49,6 @@ namespace alps { (*this)(*it); } - std::vector value; }; @@ -69,7 +66,6 @@ namespace alps { value += (it == ptr ? "," : "") + cast(*it); } - std::string value; }; @@ -90,7 +86,6 @@ namespace alps { visitor.value = v; } - T const & get_value() { return visitor.value; } diff --git a/src/alps/ngs/lib/params.cpp b/src/alps/ngs/lib/params.cpp index 2d6a057d0..37459c7e5 100644 --- a/src/alps/ngs/lib/params.cpp +++ b/src/alps/ngs/lib/params.cpp @@ -38,7 +38,6 @@ namespace alps { } } - std::size_t params::size() const { return keys.size(); } diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index 194b85d47..dc9c7faa1 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -21,7 +21,6 @@ namespace alps { namespace detail { - struct paramvalue_saver: public boost::static_visitor<> { paramvalue_saver(hdf5::archive & a) @@ -31,7 +30,6 @@ namespace alps { template void operator()(T const & v) const { ar[""] << v; } - hdf5::archive & ar; }; @@ -44,7 +42,6 @@ namespace alps { template void operator()(U const & v) const { os << short_print(v); } - private: diff --git a/src/alps/ngs/params.hpp b/src/alps/ngs/params.hpp index d625a107c..b185790db 100644 --- a/src/alps/ngs/params.hpp +++ b/src/alps/ngs/params.hpp @@ -20,7 +20,6 @@ #include #include - #include #include #include @@ -60,7 +59,6 @@ namespace alps { params(boost::filesystem::path const &); - std::size_t size() const; void erase(std::string const &); diff --git a/src/alps/ngs/scheduler/proto/mcbase.hpp b/src/alps/ngs/scheduler/proto/mcbase.hpp index 4cc27e64e..0b66f98c5 100644 --- a/src/alps/ngs/scheduler/proto/mcbase.hpp +++ b/src/alps/ngs/scheduler/proto/mcbase.hpp @@ -23,7 +23,6 @@ #include // TODO: replace by new alea #include - #include #include @@ -142,7 +141,6 @@ namespace alps { return !stop_callback(); } - result_names_type result_names() const { result_names_type names; @@ -251,7 +249,6 @@ namespace alps { mutex mutable result_mutex; private: - status_type m_status; }; diff --git a/test/pyalps/mcdata_test.py b/test/pyalps/mcdata_test.py index 6e7b7b038..7f97a56e6 100644 --- a/test/pyalps/mcdata_test.py +++ b/test/pyalps/mcdata_test.py @@ -22,8 +22,8 @@ def assert_scalar(value, mean, error): - assert np.isclose(value.mean, mean, rtol=1e-9), (value.mean, mean) - assert np.isclose(value.error, error, rtol=1e-9), (value.error, error) + np.testing.assert_allclose(value.mean, mean, rtol=1e-9) + np.testing.assert_allclose(value.error, error, rtol=1e-9) def assert_vector(value, means, errors): diff --git a/test/pyalps/pyhdf5io_test.py b/test/pyalps/pyhdf5io_test.py index 2ee96a37f..b955af4fe 100644 --- a/test/pyalps/pyhdf5io_test.py +++ b/test/pyalps/pyhdf5io_test.py @@ -28,6 +28,7 @@ def _write_all(ar): a = np.array([1, 2, 3]) + b = np.array([1.1, 2.0, 3.5]) c = np.array([1.1 + 1j, 2.0j, 3.5]) d = {"a": a, 2 + 3j: "foo"} @@ -36,10 +37,10 @@ def _write_all(ar): ar["/tuple"] = (1, 2, 3) ar["/dict"] = {"scalar": 1, "numpy": a, "numpycpx": c, "list": [1, 2, 3], "string": "str", 1: 1, 4: d} ar["/numpy"] = a - ar["/numpy2"] = np.array([1.1, 2.0, 3.5]) + ar["/numpy2"] = b ar["/numpy3"] = c ar["/numpyel"] = a[0] - ar["/numpyel2"] = np.array([1.1, 2.0, 3.5])[0] + ar["/numpyel2"] = b[0] ar["/numpyel3"] = c[0] ar["/int"] = int(1) ar["/long"] = 1 @@ -60,6 +61,14 @@ def _write_all(ar): ar["/boollist"] = [True, False] ar["/mixedlist"] = [1, 2.5] ar["/biglist"] = [2 ** 40, 2 ** 41] + ar["/npscalars"] = list(np.arange(3)) # numpy.int64 scalars + ar["/npboollist"] = list(np.array([True, False])) + ar["/numpylist3"] = [np.arange(3), [3, 4, 5]] # ndarray/list mix, legacy stacked + ar["/boolmix"] = [np.arange(2), [True, False]] # bool leaves veto stacking + ar["/longmixed"] = [1, 2.5, "x"] + list(range(10)) # 13-child group + ar["/emptylist"] = [] + ar["/shrink"] = [1, "a", "b"] + ar["/shrink"] = [1, "a"] # group re-save must drop stale children def _assert_int_array(value, expected, dtype=np.int32): @@ -77,7 +86,7 @@ def test_hdf5io(): ar = hdf5.archive(path, "r") - assert len(ar.list_children("/")) == 28 + assert len(ar.list_children("/")) == 35 # homogeneous int lists/tuples keep the int element type on disk _assert_int_array(ar["/list"], [1, 2, 3]) @@ -175,6 +184,43 @@ def test_hdf5io(): assert np.issubdtype(bl.dtype, np.integer) np.testing.assert_array_equal(bl, [2 ** 40, 2 ** 41]) + # regression: numpy-scalar lists keep their dtype in one dataset + # (numpy.int64 etc. were vectorizable in the legacy build) + nps = ar["/npscalars"] + assert isinstance(nps, np.ndarray) and np.issubdtype(nps.dtype, np.integer) + np.testing.assert_array_equal(nps, [0, 1, 2]) + # HDF5 has no native bool: bool arrays are stored (and read + # back) as their int8 storage type; only the values survive + npb = ar["/npboollist"] + assert isinstance(npb, np.ndarray) + assert npb.dtype == np.bool_ or npb.dtype == np.int8 + np.testing.assert_array_equal(npb, [1, 0]) + + # regression: rectangular ndarray/list mixes stack, like legacy + nl3 = ar["/numpylist3"] + assert isinstance(nl3, np.ndarray) and nl3.shape == (2, 3) + np.testing.assert_array_equal(nl3, [[0, 1, 2], [3, 4, 5]]) + + # regression: plain bools among the leaves veto stacking — numpy + # would silently promote True to 1 + bm = ar["/boolmix"] + assert isinstance(bm, list) and len(bm) == 2 + np.testing.assert_array_equal(bm[0], [0, 1]) + assert bm[1] == [True, False] + + # regression: a group-saved list with more than ten elements + # still loads as a list, in order (children come back from HDF5 + # lexicographically) + lm = ar["/longmixed"] + assert lm == [1, 2.5, "x"] + list(range(10)), lm + + # regression: empty lists stay integer-typed datasets + el = ar["/emptylist"] + assert len(el) == 0 + + # regression: re-saving a group-shaped list drops stale children + assert ar["/shrink"] == [1, "a"] + del ar diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 3e54bd13a..2ced13624 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -265,12 +265,40 @@ def test_params_mapping_equality_and_value_ladder(): raise AssertionError("None must be rejected") except TypeError as error: assert "None" in str(error) - # oversized integers raise instead of truncating silently + # oversized integers raise instead of truncating silently — + # inside lists too, where the double-widening fallback would + # otherwise corrupt values beyond 2**53 try: p["n"] = 2 ** 40 raise AssertionError("2**40 must be rejected") except TypeError as error: assert "32-bit" in str(error) + try: + p["nl"] = [2 ** 53 + 1] + raise AssertionError("[2**53+1] must be rejected") + except TypeError as error: + assert "32-bit" in str(error) + # bools (numpy bools included) never coerce to numbers + for bad in ([True, False], [np.bool_(True)]): + try: + p["flags"] = bad + raise AssertionError("bool list must be rejected") + except TypeError: + pass + # numpy integer scalars are accepted like numpy floats are — + # as scalars and inside lists, with the same 32-bit range policy + p["npint"] = np.int64(8) + assert p["npint"] == 8 and type(p["npint"]) is int + p["npbool"] = np.bool_(True) + assert p["npbool"] is True + p["npints"] = [np.int64(1), np.int64(2)] + assert p["npints"] == [1, 2] + assert all(type(v) is int for v in p["npints"]) + try: + p["npbig"] = [np.int64(2 ** 40)] + raise AssertionError("[np.int64(2**40)] must be rejected") + except TypeError as error: + assert "32-bit" in str(error) # exact-type lists round-trip with their element type p["ilist"] = [1, 2, 3] assert p["ilist"] == [1, 2, 3] @@ -297,6 +325,18 @@ def test_observable_lshift_chains(): assert ngs.observable2result(observable).count == 2 +def test_observables_item_deletion(): + from pyalps import ngs + + observables = ngs.observables() + observables.createRealObservable("a") + observables.createRealObservable("b") + del observables["a"] + assert "a" not in observables and "b" in observables + observables.clear() + assert len(observables) == 0 + + def test_mcbase_save_load_overrides_reach_cpp_dispatch(): from pyalps import ngs from pyalps.cxx import pyngshdf5_c @@ -385,6 +425,7 @@ def GetProperties(self, filenames): test_optional_application_extension_surface, test_params_mapping_equality_and_value_ladder, test_observable_lshift_chains, + test_observables_item_deletion, test_mcbase_save_load_overrides_reach_cpp_dispatch, test_accumulator_result_inplace_identity, ): From 76504b9225bbbd1795556a83adfebeb74b2a6f53 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Tue, 18 Aug 2026 20:01:47 -0500 Subject: [PATCH 21/52] fix(pyalps): address post-audit HDF5 regressions --- bindings/python/pyalps/README.md | 12 ++-- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 84 ++++++++++++++++++++--- parms1.h5 | Bin 6176 -> 0 bytes parms2.h5 | Bin 6176 -> 0 bytes py.h5 | Bin 7352 -> 0 bytes test/pyalps/pyhdf5_test.py | 33 ++++----- test/pyalps/pyhdf5io_test.py | 85 ++++++++++++++++++++++++ test/pyalps/pyparams_test.py | 35 +++++----- 8 files changed, 201 insertions(+), 48 deletions(-) delete mode 100644 parms1.h5 delete mode 100644 parms2.h5 delete mode 100644 py.h5 diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index ced5199a3..7622d3e6f 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -48,10 +48,8 @@ into neither of nanobind's special ABI modes: singleton, `mcdata`'s lazily-computed statistics). Do not add `FREE_THREADED` to `nanobind_add_module` without first making that state thread-safe. -- **Stable ABI (abi3):** the bindings contain no limited-API violations - (the last one, a `PyTuple_SET_ITEM`, was removed deliberately to keep - this option open), but per-version wheels are kept because the wheel - matrix is fully automated, linked abi3 would raise the floor to - CPython 3.12, and split mode adds a runtime dependency plus per-call - overhead on hot accessor paths. Revisit when a new CPython release - makes day-one support pressing. +- **Stable ABI (abi3):** not enabled or currently supported. Some binding + paths still inspect CPython type internals (`tp_name`), and no abi3 build + runs in CI. Per-version wheels are deliberate; do not add `STABLE_ABI` + until the code is limited-API clean and CI compiles and imports the + resulting extensions. diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 7d6729b2a..faf037547 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -36,6 +36,28 @@ namespace nb = nanobind; namespace alps { namespace detail { + // Decode one layer of the two entities produced by + // archive::encode_segment. Do this locally instead of calling + // archive::decode_segment unconditionally: HDF5 files created by + // other tools may legitimately contain a raw '&' in a child name. + // A literal name containing exactly "&" or "/" remains + // ambiguous because the existing ALPS format has no type marker. + static std::string decode_dict_key(std::string const & segment) { + std::string result; + result.reserve(segment.size()); + for (std::size_t pos = 0; pos < segment.size();) { + if (segment.compare(pos, 5, "&") == 0) { + result.push_back('&'); + pos += 5; + } else if (segment.compare(pos, 5, "/") == 0) { + result.push_back('/'); + pos += 5; + } else { + result.push_back(segment[pos++]); + } + } + return result; + } // Analysis of a Python list/tuple tree against the legacy // Boost.Python vectorization rules (src/alps/hdf5/python.cpp, // is_vectorizable_generic): a list is written as one dataset @@ -220,24 +242,50 @@ namespace alps { static bool is_ndarray(PyObject * raw) { return std::strcmp(Py_TYPE(raw)->tp_name, "numpy.ndarray") == 0; } + static bool is_numpy_scalar(PyObject * raw) { + static std::array const scalar_types{{ + "numpy.str_", "numpy.str", "numpy.bool_", "numpy.bool", + "numpy.int8", "numpy.int16", "numpy.int32", "numpy.int64", + "numpy.uint8", "numpy.uint16", "numpy.uint32", "numpy.uint64", + "numpy.float32", "numpy.float64", + "numpy.complex64", "numpy.complex128", + }}; + for (char const * scalar_type : scalar_types) + if (std::strcmp(Py_TYPE(raw)->tp_name, scalar_type) == 0) + return true; + return false; + } struct tree_scan { bool has_ndarray = false; + bool has_numpy_scalar = false; + bool has_other_scalar = false; bool has_bool_leaf = false; + bool homogeneous_numpy_scalars = true; + PyTypeObject * numpy_scalar_type = nullptr; }; static void scan_tree(nb::handle node, tree_scan & scan) { std::size_t const n = nb::len(node); for (std::size_t i = 0; i < n; ++i) { nb::object item = node[i]; PyObject * raw = item.ptr(); - if (is_ndarray(raw)) + if (is_ndarray(raw)) { scan.has_ndarray = true; - else if (PyList_Check(raw) || PyTuple_Check(raw)) + } else if (PyList_Check(raw) || PyTuple_Check(raw)) { scan_tree(item, scan); - else if (PyBool_Check(raw) - || std::strncmp(Py_TYPE(raw)->tp_name, "numpy.bool", 10) == 0) - scan.has_bool_leaf = true; - if (scan.has_bool_leaf) - return; // verdict fixed: bool leaves veto stacking + } else if (is_numpy_scalar(raw)) { + scan.has_numpy_scalar = true; + PyTypeObject * scalar_type = Py_TYPE(raw); + if (!scan.numpy_scalar_type) + scan.numpy_scalar_type = scalar_type; + else if (scan.numpy_scalar_type != scalar_type) + scan.homogeneous_numpy_scalars = false; + if (std::strncmp(Py_TYPE(raw)->tp_name, "numpy.bool", 10) == 0) + scan.has_bool_leaf = true; + } else { + scan.has_other_scalar = true; + if (PyBool_Check(raw)) + scan.has_bool_leaf = true; + } } } // The list shapes the legacy build stacked into one @@ -274,6 +322,16 @@ namespace alps { return false; tree_scan scan; scan_tree(l, scan); + // A rectangular tree made solely from one exact NumPy + // scalar type is vectorizable at any nesting depth. + // np.asarray below performs the final rectangularity + // check and preserves the scalar dtype. + if (scan.has_numpy_scalar && !scan.has_ndarray + && !scan.has_other_scalar) + return scan.homogeneous_numpy_scalars; + // Preserve the legacy ndarray/list stacking path. Bool + // leaves remain a veto because NumPy would silently turn + // them into 0/1 when combined with a numeric ndarray. return scan.has_ndarray && !scan.has_bool_leaf; } void operator()(nb::dict const & d) const { @@ -287,7 +345,7 @@ namespace alps { ar.create_group(path); for (auto item : d) { std::string key = nb::cast(nb::str(item.first)); - std::string child = path + "/" + key; + std::string child = path + "/" + ar.encode_segment(key); hdf5_save_py11_visitor child_visitor{ar, child}; extract_from_pyobject_py11(child_visitor, item.second); } @@ -343,7 +401,9 @@ namespace alps { // value[cast(name)]. if (ar.is_group(path)) { auto children = ar.list_children(path); - bool list_shaped = true; + // Match the legacy dynamic loader: an empty group is a + // dict. Empty lists use the dataset representation. + bool list_shaped = !children.empty(); std::vector seen(children.size(), false); for (auto const & child : children) { bool numeric = !child.empty() && child.size() < 20; @@ -370,9 +430,11 @@ namespace alps { return nb::object(std::move(result)); } else { nb::dict result; - for (auto const & child : children) - result[nb::str(child.c_str())] = + for (auto const & child : children) { + std::string const key = decode_dict_key(child); + result[nb::str(key.c_str())] = python_hdf5_load_impl(ar, path + "/" + child); + } return nb::object(std::move(result)); } } diff --git a/parms1.h5 b/parms1.h5 deleted file mode 100644 index a3af53cf4ba9ea14abcde8afa8dacb4be44887a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6176 zcmeD5aB<`1lHy|G;9!7(|4^VH0TD5PN=(a2)$#Xm31ZUucd;c48E;`y0;^$Wg(`&^ zflhNVF))IZu!)06MivkW0xV^TIfnlsY`7#?B~;Q#nh7GyCIX@vS)g17#zb>wR2e9j zfw3YOYQBH~NJTdrL>1%ROgF-(Dh73>FQ zE)F4(dCZIq(2#?BW*@|!p-iexQWGU2Y(zj|BMAy0pl3lC5@j5)Fk*s*5eqboz!3oo zm~9|pXo8igSJx5|HU!g+2sC>Qr*t#rl1>se!bV~khRxqJ&X?2(8<}AkHrF4nboY1m z@P?J{usp$-ln<>^q9vlehh}*(s9miEbRDRg6M+`cpaR_8+203TkV8vrNTCi<2l9-G z5r{;98~h0NC~GtXMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU}%H@sJ{;$ H`UcVfD$$NH diff --git a/parms2.h5 b/parms2.h5 deleted file mode 100644 index d879fd8a56e42e800509220c796aa68bcf9ac4df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6176 zcmeD5aB<`1lHy|G;9!7(|4^VH0TD5PN=(a2)$#Xm31ZUucd;c48E;`y0;^$Wgerv@ zflhNVF@Tk@$$}IzvOtwHa1|sLCFZ7U7|JFN(F5f&u#_d{7_vb_1SZK0Q45wdQi3{1Kma5f z#SW2!gggU-op;_Tuy=HkV+O=#V1&2~WD_$FC;|k4kqxm)fq@kqCCpqLLLl>iF$hfv z@VMCrv1cfgYLnDNsS!3T!!T^}8xMS^M%cg#kl~bW^q4Q$5fL^bpgbiB%2&Wl0>Y5o z&H>9?Ot8Gg15H2R3=GOp+d#z71kD16{t*#21j`#4Xe~3G(#`dUE8YE_J-lJ1Kg@58 z6?SZp*#2>7TmE2(cTh?NRWe#Y45}YwpeYn2?(XdG15U}%Q5bVSny#f?BB2o|&r6kV6xz1NJt2Rh@hvidW#~od+t4FTE;PrL|C}9?0ommxj(--=L~%JcBZGd zD%p^9To?FSqw87PJUL2+mHGZ(*-X!A=kV&%&9Uxz7;Hk5V{XeAyc?wPqAZ$X@naUdyxnB!?+fhMS6XuI21vjuLd zk(+(ZokW2T?R%hoO_3kwC`KFwPWH5N$?2LIsx{nvSvFz7=m}Ac`*MU3?4FErGp^#U z?khouh8pD?hlO90*3PF~EgVl=PqpE^5>o1?{4kdrnVcw&=cjWy?YbtET)@Q$xPWbU z<{F)SaDA(ORF83{mW6*_F!_5p{5OuZE}qMFXZoYovPPquwEvR6671jFm}ohd zvZxC3It}UFsSOzQumI2FvWxz6E8L$jg0t1_fQ!EFBMJcZTO`Q77I>hkK5b^aV ze2-R_GD5WC8XP6su2Yw2al!N8EMT45G=NPdWHwZr_;i5P7*gkePfSR60Tg8PHP3}g z!80pW_o6xWg=oiJIC8x%&Z^u-xCPz32gjD+BdCjYbH>2nYcseZ^r-G_x6a;c=G%_@ zQs(TS2}eF_U<{7lcp47BFmgD;Rc;xH(T8s{P1Lw6veUmleDUVvg4T*_u&0P|a5#nH XH42afnK(|IiOX}1W}bt`PYwMBBN~h} diff --git a/test/pyalps/pyhdf5_test.py b/test/pyalps/pyhdf5_test.py index efee3a53a..1f43f0736 100644 --- a/test/pyalps/pyhdf5_test.py +++ b/test/pyalps/pyhdf5_test.py @@ -15,7 +15,8 @@ import pyalps.hdf5 as h5 import numpy as np -import sys +import os +import tempfile def write(ar): ar["/int"] = 9 @@ -56,19 +57,21 @@ def read(ar): raise Exception('invalid array value') def test_hdf5(): - oar = h5.archive("py.h5", 'w') - write(oar) - del oar - - iar = h5.archive("py.h5", 'r') - if iar.is_complex("/int") or not iar.is_complex("/cplx") or not iar.extent("/np/cplx"): - raise Exception('invalid complex detection') - read(iar) - del iar - - ar = h5.archive("py.h5", 'w') - write(ar) - read(ar) - del ar + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "py.h5") + oar = h5.archive(path, 'w') + write(oar) + del oar + + iar = h5.archive(path, 'r') + if iar.is_complex("/int") or not iar.is_complex("/cplx") or not iar.extent("/np/cplx"): + raise Exception('invalid complex detection') + read(iar) + del iar + + ar = h5.archive(path, 'w') + write(ar) + read(ar) + del ar print("SUCCESS") diff --git a/test/pyalps/pyhdf5io_test.py b/test/pyalps/pyhdf5io_test.py index b955af4fe..5e21e81d3 100644 --- a/test/pyalps/pyhdf5io_test.py +++ b/test/pyalps/pyhdf5io_test.py @@ -224,6 +224,91 @@ def test_hdf5io(): del ar +def test_hdf5_empty_dict_roundtrip(): + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "empty-dict.h5") + ar = hdf5.archive(path, "w") + ar["/value"] = {} + del ar + + ar = hdf5.archive(path, "r") + value = ar["/value"] + assert type(value) is dict + assert value == {} + del ar + + +def test_hdf5_dict_key_roundtrip(): + expected = { + "a/b": 1, + "a": {"b": 2}, + "amp&key": 3, + "entity/": 4, + } + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "dict-keys.h5") + ar = hdf5.archive(path, "w") + ar["/value"] = expected + ar.create_group("/rawamp") + ar["/rawamp/literal&child"] = 5 + del ar + + ar = hdf5.archive(path, "r") + assert ar["/value"] == expected + # A raw ampersand from a non-pyalps HDF5 producer is not an + # encoded path entity and must remain literal. + assert ar["/rawamp"] == {"literal&child": 5} + del ar + + +def test_hdf5_nested_numpy_scalar_vectorization(): + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "nested-numpy-scalars.h5") + ar = hdf5.archive(path, "w") + ar["/rectangular"] = [ + [np.int16(1), np.int16(2)], + [np.int16(3), np.int16(4)], + ] + ar["/ragged"] = [ + [np.int16(1)], + [np.int16(2), np.int16(3)], + ] + ar["/mixed"] = [ + [np.int16(1), np.int16(2)], + [np.int32(3), np.int32(4)], + ] + ar["/arrayandscalar"] = [np.arange(2), np.int64(3)] + del ar + + ar = hdf5.archive(path, "r") + rectangular = ar["/rectangular"] + assert isinstance(rectangular, np.ndarray) + assert rectangular.dtype == np.int16 + assert rectangular.shape == (2, 2) + np.testing.assert_array_equal(rectangular, [[1, 2], [3, 4]]) + + # The new recursive case must not widen its acceptance: ragged + # trees, mixed NumPy scalar dtypes, and sequence/scalar mixtures + # retain the existing group representation. + ragged = ar["/ragged"] + assert isinstance(ragged, list) and len(ragged) == 2 + assert all(row.dtype == np.int16 for row in ragged) + np.testing.assert_array_equal(ragged[0], [1]) + np.testing.assert_array_equal(ragged[1], [2, 3]) + mixed = ar["/mixed"] + assert isinstance(mixed, list) and len(mixed) == 2 + assert mixed[0].dtype == np.int16 + assert mixed[1].dtype == np.int32 + array_and_scalar = ar["/arrayandscalar"] + assert isinstance(array_and_scalar, list) + np.testing.assert_array_equal(array_and_scalar[0], [0, 1]) + assert array_and_scalar[1] == 3 + del ar + + if __name__ == "__main__": test_hdf5io() + test_hdf5_empty_dict_roundtrip() + test_hdf5_dict_key_roundtrip() + test_hdf5_nested_numpy_scalar_vectorization() print("SUCCESS") diff --git a/test/pyalps/pyparams_test.py b/test/pyalps/pyparams_test.py index 98336b7b6..7436c7660 100644 --- a/test/pyalps/pyparams_test.py +++ b/test/pyalps/pyparams_test.py @@ -14,7 +14,8 @@ import pyalps.hdf5 as hdf5 import pyalps.ngs as ngs -import sys +import os +import tempfile orig_dict = { 'val1' : 42, @@ -43,23 +44,27 @@ def test_params(): ## Check nonetype assert type(p["undefined"]) == type(None) - ## Write to hdf5 - with hdf5.archive('parms1.h5', 'w') as oar: - p.save(oar) # does not use path '/parameters' - - with hdf5.archive('parms2.h5', 'w') as oar: - for key in sorted(p.keys()): - print(key) - oar['parameters/' + key] = p[key] - ## Load from hdf5 - with hdf5.archive('parms2.h5', 'r') as oar: - iar = hdf5.archive('parms2.h5', 'r') - p.load(iar) - + ## Write to and load from hdf5 without leaving test artifacts in the tree. + with tempfile.TemporaryDirectory() as directory: + parms1 = os.path.join(directory, 'parms1.h5') + parms2 = os.path.join(directory, 'parms2.h5') + with hdf5.archive(parms1, 'w') as oar: + p.save(oar) # does not use path '/parameters' + + with hdf5.archive(parms2, 'w') as oar: + for key in sorted(p.keys()): + print(key) + oar['parameters/' + key] = p[key] + + # Preserve the existing simultaneous-reader exercise. + with hdf5.archive(parms2, 'r'): + with hdf5.archive(parms2, 'r') as iar: + p.load(iar) + for k in sorted(orig_dict.keys()): assert p[k] == orig_dict[k] assert_type(p, k) print(k,'ok!') if __name__ == '__main__': - test_params() \ No newline at end of file + test_params() From c217c6ffb12a4055aafcccbf29e2858d976bd2b8 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 01:20:20 -0500 Subject: [PATCH 22/52] fix(pyalps): complete nanobind compatibility migration --- .github/workflows/build.yml | 13 + .github/workflows/build_wheels.yml | 32 +- bindings/python/pyalps/MIGRATION.md | 68 ++++ bindings/python/pyalps/README.md | 3 + bindings/python/pyalps/cpp/dict_to_params.hpp | 298 ++++++++++---- .../pyalps/cpp/ngs/extract_from_pyobject.hpp | 18 +- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 33 +- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 36 +- bindings/python/pyalps/cpp/ngs/params.cpp | 2 +- bindings/python/pyalps/pyproject.toml | 34 ++ bindings/python/pyalps/src/pyalps/__init__.py | 43 +- bindings/python/pyalps/src/pyalps/mpi.py | 381 ++++++++++++++++-- src/alps/ngs/detail/export_sim_to_python.hpp | 107 +++++ src/alps/ngs/detail/paramproxy.hpp | 5 +- src/alps/ngs/detail/paramvalue.hpp | 9 +- src/alps/ngs/detail/paramvalue_reader.hpp | 17 +- src/alps/ngs/lib/paramproxy.cpp | 2 +- src/alps/ngs/lib/paramvalue.cpp | 1 + src/alps/python/make_copy.hpp | 20 + src/alps/python/save_observable_to_hdf5.hpp | 25 ++ test/ngs/params/assign.cpp | 7 + test/pyalps/pyhdf5io_test.py | 95 +++++ test/pyalps/test_binding_surface.py | 353 +++++++++++++++- tutorials/ngs/5_export_python/CMakeLists.txt | 42 ++ tutorials/ngs/5_export_python/README.md | 20 + tutorials/ngs/5_export_python/export2py.cpp | 12 + tutorials/ngs/5_export_python/ising.cpp | 42 ++ tutorials/ngs/5_export_python/ising.hpp | 28 ++ tutorials/ngs/5_export_python/main.py | 12 + tutorials/ngs/5_export_python/smoke_test.py | 38 ++ 30 files changed, 1651 insertions(+), 145 deletions(-) create mode 100644 bindings/python/pyalps/MIGRATION.md create mode 100644 src/alps/ngs/detail/export_sim_to_python.hpp create mode 100644 src/alps/python/make_copy.hpp create mode 100644 src/alps/python/save_observable_to_hdf5.hpp create mode 100644 tutorials/ngs/5_export_python/CMakeLists.txt create mode 100644 tutorials/ngs/5_export_python/README.md create mode 100644 tutorials/ngs/5_export_python/export2py.cpp create mode 100644 tutorials/ngs/5_export_python/ising.cpp create mode 100644 tutorials/ngs/5_export_python/ising.hpp create mode 100644 tutorials/ngs/5_export_python/main.py create mode 100644 tutorials/ngs/5_export_python/smoke_test.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7bac762fa..8c0e113d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,11 +94,24 @@ jobs: run: | cmake -S $GITHUB_WORKSPACE -B build \ -DBoost_ROOT_DIR=`pwd`/boost_1_${{ matrix.plat.boost_version }}_0 \ + -DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/build/install" \ -DCMAKE_CXX_STANDARD=${{ matrix.plat.cxx_standard || '17' }} \ -DCMAKE_CXX_FLAGS="-fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" cmake --build build -j 2 cmake --build build -j 2 -t test + # Compile the public exporter as a downstream project, rather than only + # checking that its compatibility header is present in the install. + - name: Smoke test downstream nanobind simulation extension + if: matrix.plat.os == 'ubuntu-24.04' && matrix.plat.c_compiler == 'gcc' && matrix.plat.c_version == 14 && matrix.plat.py_version == '3.14' && matrix.plat.boost_version == 91 && matrix.plat.cxx_standard == null + run: | + python -m pip install "nanobind>=2.10,<3" + cmake --install build + cmake -S tutorials/ngs/5_export_python -B downstream-export-build \ + -DALPS_DIR="$PWD/build/install/share/alps" \ + -DPython_EXECUTABLE="$(command -v python)" + cmake --build downstream-export-build -j 2 + macos-build: name: Build ALPS on ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }} runs-on: ${{ matrix.plat.os }} diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 287269f1d..afa818b45 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -115,8 +115,38 @@ jobs: python -c "import pyalps, pyalps.alea, pyalps.hdf5, pyalps.pytools; print(pyalps.__file__)" python -m pytest -q test/pyalps + # The ordinary wheel tests deliberately keep mpi4py optional. This job + # installs one consistent Open MPI stack and verifies real inter-rank + # collectives, requests and point-to-point traffic through pyalps.mpi. + mpi_smoke: + name: Smoke test MPI adapter with two ranks + needs: [build_wheels] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - uses: actions/download-artifact@v8 + with: + pattern: cibw-wheels-ubuntu-latest-* + path: wheelhouse + merge-multiple: true + + - name: Install wheel and MPI runtime + run: | + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin + python -m pip install numpy scipy pytest mpi4py + python -m pip install --no-index --no-deps --find-links wheelhouse pyalps + + - name: Run two-rank compatibility surface + run: mpiexec -n 2 python -m pytest -q test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface + upload_pypi: - needs: [build_wheels, build_sdist, smoke_test] + needs: [build_wheels, build_sdist, smoke_test, mpi_smoke] runs-on: ubuntu-latest environment: pypi permissions: diff --git a/bindings/python/pyalps/MIGRATION.md b/bindings/python/pyalps/MIGRATION.md new file mode 100644 index 000000000..213764687 --- /dev/null +++ b/bindings/python/pyalps/MIGRATION.md @@ -0,0 +1,68 @@ +# Migrating from the Boost.Python pyalps build + +The public Python API is preserved wherever it maps to native ALPS values. +The nanobind build intentionally does not keep arbitrary Python objects inside +`alps::params` or the C++ library. + +## Parameters + +`pyalps.ngs.params` accepts native booleans, 32-bit integers, floating-point +and complex numbers, strings, homogeneous Python sequences, NumPy scalar +arrays, and one-dimensional NumPy arrays. Values are copied into native C++ +storage. Multidimensional arrays, `None`, dictionaries, arbitrary objects, and +integers outside ALPS' 32-bit parameter range raise `TypeError` rather than +being retained as opaque Python objects. Sequence values are returned as +lists, irrespective of whether the input was a list, tuple, or NumPy array. + +## MPI + +Install `pyalps[mpi]` to use `pyalps.mpi`. The module provides the commonly +used Boost.MPI Python surface (`world`, `rank`, `size`, `Communicator`, +point-to-point methods, collectives, status/request names, and `Timer`) on top +of mpi4py. Ordinary pyalps wheels remain independent of any MPI runtime. + +The historical `mcbase(..., communicator)` argument is still accepted. It is +ignored, as it was by the Boost.Python wrapper; `alps::mcbase` itself has no +communicator constructor. Use `pyalps.mpi` for Python communication and ALPS' +C++ MPI adapters for MPI-aware C++ simulations. + +Boost.MPI's Python-object serialization bridge and skeleton/content API are +not reproduced. Hybrid applications should use mpi4py's typed buffer API or +an application-specific native C++ protocol. + +## Compiled module paths and DWA vectors + +Legacy paths such as `pyalps.pyalea_c` and `pyalps.dwa_c` remain aliases of +the extensions now stored under `pyalps._ext`. The preferred stable import is +still `pyalps.cxx.pyalea_c` for core extensions and `pyalps.dwa` for DWA. + +DWA's former `std_vector_*` constructors are compatibility aliases for +Python's `list`. DWA methods return list snapshots, which avoids exposing +mutable C++ container proxies and accepts ordinary Python sequences directly. + +## Exporting downstream C++ simulations + +The public header `` now implements +the export helper with nanobind while keeping the +`ALPS_EXPORT_SIM_TO_PYTHON` macro. Change the module declaration in an old +export source from: + +```cpp +BOOST_PYTHON_MODULE(my_sim) { +``` + +to: + +```cpp +#include +NB_MODULE(my_sim, m) { +``` + +and keep the existing export macro call. See +`tutorials/ngs/5_export_python` for a complete standalone CMake build. + +The removed `alps/python/numpy_array.hpp` API should be replaced with +`nanobind::ndarray` or nanobind's STL casters. The old +`alps/hdf5/python.hpp` operators accepted `boost::python::object` and have no +Python-object-free equivalent; use typed `alps::hdf5::archive` operations in +C++ or `pyalps.hdf5` at the Python boundary. diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 7622d3e6f..126aeb3fc 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -8,6 +8,9 @@ python -m pip install pyalps ``` Install `pyalps[plot]` to use the Matplotlib plotting helpers. +Install `pyalps[mpi]` for the mpi4py-backed `pyalps.mpi` compatibility layer. +Projects moving from the Boost.Python build should also read +[the nanobind migration guide](https://github.com/ALPSim/ALPS/blob/master/bindings/python/pyalps/MIGRATION.md). The bindings are built as a standalone `scikit-build-core` project using nanobind. A source build requires Python 3.10 or newer, CMake 3.21 or newer, diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index c4e197129..9b7bb8cb4 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -21,6 +21,15 @@ namespace pyalps { namespace nb = nanobind; namespace detail { +enum class scalar_kind { + unsupported, + boolean, + integer, + real, + complex, + string +}; + inline bool is_bool_like(PyObject * raw) { // plain bool, or a numpy bool scalar (numpy.bool_ / numpy.bool), // which does NOT subclass bool and would otherwise slip through @@ -28,101 +37,254 @@ inline bool is_bool_like(PyObject * raw) { return PyBool_Check(raw) || std::strncmp(Py_TYPE(raw)->tp_name, "numpy.bool", 10) == 0; } +inline bool is_numpy_array(nb::handle value) { + // isinstance, rather than an exact tp_name comparison, keeps ndarray + // subclasses (for example an unmasked numpy.ma.MaskedArray) on the same + // native-copy path. Import lookup itself is cached by Python. + return nb::isinstance(value, nb::module_::import_("numpy").attr("ndarray")); +} + +inline char numpy_scalar_kind(nb::handle value) { + nb::object numpy = nb::module_::import_("numpy"); + if (!nb::isinstance(value, numpy.attr("generic"))) + return '\0'; + std::string const kind = nb::cast( + value.attr("dtype").attr("kind")); + return kind.empty() ? '\0' : kind.front(); +} + +inline scalar_kind classify_scalar(nb::handle value) { + if (is_bool_like(value.ptr())) + return scalar_kind::boolean; + + // NumPy extended scalars (float16/32/longdouble and + // complex64/clongdouble) are not consistently Python float/complex + // subclasses. Inspect dtype.kind before consulting Python's protocols so + // a complex scalar can never pass through __float__ and lose its + // imaginary component. + switch (numpy_scalar_kind(value)) { + case 'b': return scalar_kind::boolean; + case 'i': + case 'u': return scalar_kind::integer; + case 'f': return scalar_kind::real; + case 'c': return scalar_kind::complex; + case 'S': + case 'U': return scalar_kind::string; + case '\0': break; + default: return scalar_kind::unsupported; + } + + if (PyLong_Check(value.ptr()) || PyIndex_Check(value.ptr())) + return scalar_kind::integer; + if (PyFloat_Check(value.ptr())) + return scalar_kind::real; + if (PyComplex_Check(value.ptr())) + return scalar_kind::complex; + if (PyUnicode_Check(value.ptr()) || PyBytes_Check(value.ptr())) + return scalar_kind::string; + return scalar_kind::unsupported; +} + +inline int integer_value(nb::handle value, std::string const & key) { + PyObject * indexed = PyNumber_Index(value.ptr()); + if (!indexed) + throw nb::python_error(); + int overflow = 0; + long long const converted = PyLong_AsLongLongAndOverflow(indexed, &overflow); + Py_DECREF(indexed); + if (PyErr_Occurred()) { + PyErr_Clear(); + overflow = 1; + } + if (overflow + || converted < std::numeric_limits::min() + || converted > std::numeric_limits::max()) + throw nb::type_error(("parameter '" + key + + "' contains an integer that does not fit params'" + " 32-bit integer type").c_str()); + return static_cast(converted); +} + +inline double real_value(nb::handle value, std::string const & key) { + if (classify_scalar(value) == scalar_kind::integer) + return static_cast(integer_value(value, key)); + double const converted = PyFloat_AsDouble(value.ptr()); + if (PyErr_Occurred()) + throw nb::python_error(); + return converted; +} + +inline std::complex complex_value(nb::handle value, + std::string const & key) { + scalar_kind const kind = classify_scalar(value); + if (kind == scalar_kind::integer || kind == scalar_kind::real) + return std::complex(real_value(value, key), 0.0); + Py_complex const converted = PyComplex_AsCComplex(value.ptr()); + if (PyErr_Occurred()) + throw nb::python_error(); + return std::complex(converted.real, converted.imag); +} + +inline std::string string_value(nb::handle value) { + if (PyUnicode_Check(value.ptr())) + return nb::cast(value); + + // Python/NumPy byte strings map to ALPS' native UTF-8 std::string. This + // accepts the common fixed-width NumPy "S" dtype without creating an + // opaque-object escape hatch; invalid UTF-8 remains a loud error because + // nanobind must also decode the value when returning it to Python. + char * bytes = nullptr; + Py_ssize_t size = 0; + if (PyBytes_AsStringAndSize(value.ptr(), &bytes, &size) != 0) + throw nb::python_error(); + nb::object decoded = nb::steal( + PyUnicode_DecodeUTF8(bytes, size, "strict")); + if (!decoded.is_valid()) + throw nb::python_error(); + return nb::cast(decoded); +} } // namespace detail // Store one Python value under `key`. paramvalue's only integral // alternative is a 32-bit int and libalps static_casts wider integer // types down to it, so out-of-range integers are rejected loudly here // — for scalars and inside lists alike — rather than truncated or -// silently widened to double. List probes use exact element types -// first (convert=false) so integer lists round-trip as ints; mixed -// numeric lists without bools widen to double. +// silently widened to double. Sequence elements are classified before +// conversion so integers round-trip as ints, mixed real numerics widen to +// double, and complex values can never be coerced through a real-number path. inline void set_param_value(alps::params & p, std::string const & key, nb::handle value) { if (value.is_none()) throw nb::type_error(("cannot store None for parameter '" + key + "': params has no null type; delete the key instead").c_str()); - if (detail::is_bool_like(value.ptr())) { + detail::scalar_kind const scalar_type = detail::classify_scalar(value); + if (scalar_type == detail::scalar_kind::boolean) { // PyObject_IsTrue rather than nb::cast: the caster does // not convert numpy bool scalars int const truth = PyObject_IsTrue(value.ptr()); if (truth < 0) throw nb::python_error(); p[key] = (truth == 1); - } else if (nb::isinstance(value) || PyIndex_Check(value.ptr())) { - // PyIndex_Check admits numpy integer scalars (np.int64 etc.), - // which don't subclass int the way np.float64 subclasses float - try { - p[key] = nb::cast(value); - } catch (nb::cast_error const &) { + } else if (detail::is_numpy_array(value)) { + // params is deliberately Python-object-free. Convert the NumPy + // value once at the boundary and store one of paramvalue's native + // scalar/vector alternatives. ALPS parameters are one-dimensional; + // preserving an arbitrary N-D ndarray would require an object escape + // hatch or a new tensor type in the C++ API. + std::size_t const ndim = nb::cast(value.attr("ndim")); + if (ndim == 0) { + set_param_value(p, key, value.attr("item")()); + return; + } + if (ndim != 1) throw nb::type_error(("parameter '" + key - + "' does not fit params' 32-bit integer type").c_str()); + + "' is a multidimensional numpy array; params supports only scalars" + " and one-dimensional sequences").c_str()); + nb::object items = value.attr("tolist")(); + if (nb::len(items) == 0) { + // With no elements the sequence ladder cannot infer a native + // alternative. Preserve the ndarray's scalar family explicitly + // so an empty bool array does not silently become vector. + std::string const kind = nb::cast( + value.attr("dtype").attr("kind")); + if (kind == "b") p[key] = std::vector(); + else if (kind == "i" || kind == "u") p[key] = std::vector(); + else if (kind == "f") p[key] = std::vector(); + else if (kind == "c") p[key] = std::vector>(); + else if (kind == "S" || kind == "U") p[key] = std::vector(); + else + throw nb::type_error(("parameter '" + key + + "' is an empty numpy array with unsupported dtype kind '" + + kind + "'").c_str()); + return; } - } else if (nb::isinstance(value)) { - p[key] = nb::cast(value); - } else if (PyComplex_Check(value.ptr())) { - p[key] = nb::cast>(value); - } else if (nb::isinstance(value)) { - p[key] = nb::cast(value); + set_param_value(p, key, items); + return; + } else if (scalar_type == detail::scalar_kind::integer) { + p[key] = detail::integer_value(value, key); + } else if (scalar_type == detail::scalar_kind::real) { + p[key] = detail::real_value(value, key); + } else if (scalar_type == detail::scalar_kind::complex) { + p[key] = detail::complex_value(value, key); + } else if (scalar_type == detail::scalar_kind::string) { + p[key] = detail::string_value(value); } else if (nb::isinstance(value) || nb::isinstance(value)) { - // One pre-scan enforcing the loud-failure policies explicitly, - // independent of caster conversion behaviour: bools never - // coerce to numbers, and oversized integers raise exactly like - // the scalar arm instead of widening to double (which would - // corrupt values beyond 2^53). + // Classify before conversion. In particular, NumPy complex scalars + // implement a warning-emitting __float__; blindly probing a + // vector caster first would discard their imaginary parts. std::size_t const length = nb::len(value); bool has_bool = false; + bool has_integer = false; + bool has_real = false; + bool has_complex = false; + bool has_string = false; for (std::size_t i = 0; i < length; ++i) { nb::object item = value[i]; - PyObject * raw = item.ptr(); - if (detail::is_bool_like(raw)) { - has_bool = true; - } else if (PyLong_Check(raw) || PyIndex_Check(raw)) { - // PyNumber_Index covers numpy integer scalars too — - // they are not PyLong subclasses but must obey the - // same 32-bit range policy - PyObject * as_long = PyNumber_Index(raw); - if (!as_long) { - PyErr_Clear(); - continue; - } - int overflow = 0; - long long v = PyLong_AsLongLongAndOverflow(as_long, &overflow); - Py_DECREF(as_long); - if (overflow - || v < std::numeric_limits::min() - || v > std::numeric_limits::max()) - throw nb::type_error(("parameter '" + key - + "' contains an integer that does not fit params'" - " 32-bit integer type").c_str()); + switch (detail::classify_scalar(item)) { + case detail::scalar_kind::boolean: has_bool = true; break; + case detail::scalar_kind::integer: + detail::integer_value(item, key); // range check now + has_integer = true; + break; + case detail::scalar_kind::real: has_real = true; break; + case detail::scalar_kind::complex: has_complex = true; break; + case detail::scalar_kind::string: has_string = true; break; + case detail::scalar_kind::unsupported: + throw nb::type_error(("unsupported element in parameter '" + + key + "' sequence").c_str()); + } + } + + bool const has_non_bool = has_integer || has_real || has_complex || has_string; + if (has_bool && !has_non_bool) { + std::vector flags; + flags.reserve(length); + for (std::size_t i = 0; i < length; ++i) { + int const truth = PyObject_IsTrue(value[i].ptr()); + if (truth < 0) + throw nb::python_error(); + flags.push_back(truth == 1); } + p[key] = flags; + return; } - if (!has_bool) { - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>>(value, false); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>(value, false); return; } - catch (nb::cast_error const &) {} - // numpy integer scalars satisfy the convert=true int - // caster via __index__ (floats don't), keeping - // [np.int64(8)] consistent with the scalar np.int64 rung; - // the pre-scan above already range-checked every element - try { p[key] = nb::cast>(value); return; } - catch (nb::cast_error const &) {} - // mixed numeric content (e.g. [1, 2.5] or numpy floats) - // widens to double / complex - try { p[key] = nb::cast>(value); return; } - catch (nb::cast_error const &) {} - try { p[key] = nb::cast>>(value); return; } - catch (nb::cast_error const &) {} + if (has_bool) + throw nb::type_error(("unsupported sequence for parameter '" + key + + "' (bools cannot be mixed with other element types)").c_str()); + if (has_string && (has_integer || has_real || has_complex)) + throw nb::type_error(("unsupported sequence for parameter '" + key + + "' (strings cannot be mixed with numeric element types)").c_str()); + + if (has_string) { + std::vector strings; + strings.reserve(length); + for (std::size_t i = 0; i < length; ++i) + strings.push_back(detail::string_value(value[i])); + p[key] = strings; + } else if (has_complex) { + std::vector> numbers; + numbers.reserve(length); + for (std::size_t i = 0; i < length; ++i) + numbers.push_back(detail::complex_value(value[i], key)); + p[key] = numbers; + } else if (has_real) { + std::vector numbers; + numbers.reserve(length); + for (std::size_t i = 0; i < length; ++i) + numbers.push_back(detail::real_value(value[i], key)); + p[key] = numbers; + } else { + // An empty untyped Python sequence follows the historic native + // ladder's first vector alternative (vector). + std::vector numbers; + numbers.reserve(length); + for (std::size_t i = 0; i < length; ++i) + numbers.push_back(detail::integer_value(value[i], key)); + p[key] = numbers; } - throw nb::type_error(("unsupported list for parameter '" + key - + "' (expected homogeneous numbers or strings; bools are not" - " a parameter list type)").c_str()); + return; } else { throw nb::type_error(("unsupported type for parameter '" + key - + "' (expected bool/int/float/complex/str or a list of those)").c_str()); + + "' (expected bool/int/float/complex/str, a one-dimensional" + " numpy array, or a sequence of those scalar types)").c_str()); } } inline alps::params params_from_dict(nb::dict const & values) { diff --git a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp index 3023d4bad..735ca0f5a 100644 --- a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp +++ b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp @@ -84,17 +84,23 @@ )); else if (dtype == "numpy.complex128") visitor(nb_::cast>(data)); - else if (dtype == "numpy.ndarray") { + else if (dtype == "numpy.ndarray" + || nb_::isinstance( + data, + nb_::module_::import_("numpy").attr("ndarray"))) { + // Reject non-native byte order explicitly. nanobind's + // failed ndarray cast would otherwise surface only as + // the unhelpful message "std::bad_cast". + if (!nb_::cast(data.attr("dtype").attr("isnative"))) + throw std::runtime_error("numpy array is not native" + ALPS_STACKTRACE); // Raw buffer access via nb::ndarray, with a strict // dtype match — nb::cast>(arr) of a // mismatched-dtype array silently coerces (e.g. // int → bool yields all-true), so we inspect // .dtype() ourselves and pick the matching arm. - // We require C-contiguity; the typical save path - // is bulk contiguous data and silently copying - // behind the user's back was the old - // PyArray_GETCONTIGUOUS behaviour we don't want - // to inherit. + // The C-contiguous caster preserves the legacy + // PyArray_GETCONTIGUOUS behaviour for sliced and + // transposed arrays by materialising a temporary copy. auto arr_any = nb_::cast>(data); std::vector sizes; sizes.reserve(arr_any.ndim()); diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index faf037547..616865ff8 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -153,6 +153,14 @@ namespace alps { } template void operator()(U const * ptr, std::vector const & sizes) const { + // NumPy uses rank zero for a 0-D array. Passing an empty + // size vector to archive::write creates an HDF5 NULL + // dataspace, which silently turns the scalar into an empty + // array. Store the pointed-to value as a scalar instead. + if (sizes.empty()) { + ar[path] << *ptr; + return; + } // Use make_pvp(path, ptr, size-vector) to preserve the // dimensional shape — a plain vector flatten would // round-trip the data but lose the rank. @@ -372,7 +380,13 @@ namespace alps { std::size_t total = 1; for (auto s : shape) total *= s; std::vector flat(total); - if (shape.size() <= 1) { + // archive::read rejects a zero-sized chunk. The HDF5 dataset + // already carries the complete extent, so for arrays such as + // (0, 2) and (2, 0) there is no payload to read: construct the + // correctly shaped NumPy array directly. + if (total == 0) { + return alps::python::make_numpy_array(nullptr, shape); + } else if (shape.size() <= 1) { // vector overload works directly. ar[path] >> flat; } else { @@ -447,7 +461,22 @@ namespace alps { // array. if (ar.is_complex(path)) { auto ext = ar.extent(path); - if (ext.size() == 1) { + bool const single_value = ext.size() == 1; + // Preserve the component precision. The legacy loader + // returned complex64 datasets as NumPy complex64 rather than + // widening them to complex128; only a complex128 scalar used + // the ordinary Python ``complex`` shortcut. + if (ar.is_datatype(path)) { + if (single_value) { + std::complex value; + ar[path] >> value; + return alps::python::make_numpy_array( + &value, std::vector()); + } + std::vector shape(ext.begin(), ext.end() - 1); + return load_nd_array>(ar, path, shape); + } + if (single_value) { std::complex v; ar[path] >> v; return nb::cast(v); } std::vector shape(ext.begin(), ext.end() - 1); diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index e87a90266..32e7d1892 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -46,9 +46,6 @@ #include #include namespace nb = nanobind; -#ifdef ALPS_HAVE_MPI - #include -#endif #include #include #include @@ -67,17 +64,11 @@ namespace alps { // save(archive&) / load(archive&); all five must be // forwarded so Python overrides are seen by C++ callers. NB_TRAMPOLINE(mcbase, 5); - #ifdef ALPS_HAVE_MPI - PyMCBase(nb::dict const & arg, - std::size_t seed_offset = 42, - boost::mpi::communicator const & /*comm*/ = boost::mpi::communicator()) - : mcbase(pyalps::params_from_dict(arg), seed_offset) - {} - #else - PyMCBase(nb::dict const & arg, std::size_t seed_offset = 42) - : mcbase(pyalps::params_from_dict(arg), seed_offset) - {} - #endif + PyMCBase(nb::dict const & arg, + std::size_t seed_offset = 42, + nb::handle /*communicator*/ = nb::none()) + : mcbase(pyalps::params_from_dict(arg), seed_offset) + {} void update() override { NB_OVERRIDE_PURE(update); } @@ -113,16 +104,15 @@ namespace alps { NB_MODULE(pyngsbase_c, m) { nb::class_(m, "_mcbase", nb::never_destruct()); nb::class_(m, "mcbase") - // Always expose the (dict, seed_offset) form from Python. When - // ALPS_HAVE_MPI is on we'd *like* to offer an optional - // communicator too, but boost::mpi::communicator is not a - // nanobind-registered type so nb::arg(..).default_value() can't - // materialise it. MPI simulations that actually need to hand - // Python a communicator should do so from C++ using the - // extended trampoline ctor directly. - .def(nb::init(), + // Retain the legacy third argument without binding Boost.MPI. The + // Boost.Python-era constructor accepted a communicator but never + // passed it to alps::mcbase (which has no communicator constructor), + // so accepting and ignoring it is behaviorally faithful. Python-side + // communication is provided by pyalps.mpi's mpi4py adapter. + .def(nb::init(), nb::arg("dict"), - nb::arg("seed_offset") = 42) + nb::arg("seed_offset") = 42, + nb::arg("communicator") = nb::none()) .def_prop_ro( "random", [](alps::PyMCBase & self) -> alps::random01 & { return self.get_random(); }, diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index 02df54e0f..e4e5781f0 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -39,7 +39,7 @@ nb::object paramvalue_to_py(alps::detail::paramvalue const & pv) { // via paramproxy's templated operator= — shared ladder in // ../dict_to_params.hpp so params, mcbase and the application modules // all ingest values identically. -void params_setitem(alps::params & self, nb::object const & key_obj, nb::object const & value) { +void params_setitem(alps::params & self, nb::object const & key_obj, nb::handle value) { pyalps::set_param_value(self, nb::cast(nb::str(key_obj)), value); } nb::object params_getitem(alps::params & self, nb::object const & key_obj) { diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index 06a42f388..c9195f15a 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -13,10 +13,38 @@ readme = "README.md" requires-python = ">=3.10" license = "MIT" dependencies = ["numpy>=1.26", "scipy>=1.13"] +authors = [ + { name = "Sergei Iskakov", email = "siskakov@umich.edu" }, + { name = "Fei Lin", email = "feilin.physics@gmail.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", +] + +[project.urls] +Homepage = "https://alps.comp-phys.org" +Repository = "https://github.com/ALPSim/ALPS" +Issues = "https://github.com/ALPSim/ALPS/issues" [project.optional-dependencies] plot = ["matplotlib>=3.8"] test = ["pytest>=8"] +tests = ["pytest>=8", "coverage>=7", "pytest-benchmark>=5"] +mpi = ["mpi4py>=4"] [tool.scikit-build] cmake.source-dir = "." @@ -83,6 +111,12 @@ repair-wheel-command = [ "delocate-listdeps --all {dest_dir}/*.whl", ] +[[tool.cibuildwheel.overrides]] +select = "cp314-*" +inherit.environment = "append" +environment = { PYALPS_TEST_DOWNSTREAM_EXPORT = "1" } +test-requires = ["pytest", "nanobind>=2.10,<3"] + [[tool.cibuildwheel.overrides]] select = "*-macosx_*" inherit.environment = "append" diff --git a/bindings/python/pyalps/src/pyalps/__init__.py b/bindings/python/pyalps/src/pyalps/__init__.py index 29aefeb79..250e36db6 100644 --- a/bindings/python/pyalps/src/pyalps/__init__.py +++ b/bindings/python/pyalps/src/pyalps/__init__.py @@ -19,12 +19,43 @@ from .pytools import * from .floatwitherror import FloatWithError from . import fit_wrapper +from . import cxx as cxx + + +# The extensions live in ``pyalps._ext`` in wheels, but Boost.Python-era +# installations also exposed the core modules directly below ``pyalps``. +# Register aliases instead of loading a second copy of an extension: nanobind +# has one process-wide type registry, and duplicate module instances would +# create subtly incompatible versions of the same C++ types. +for _extension_name in ( + "pyalea_c", + "pymcdata_c", + "pytools_c", + "pyngsparams_c", + "pyngshdf5_c", + "pyngsbase_c", + "pyngsobservable_c", + "pyngsobservables_c", + "pyngsresult_c", + "pyngsresults_c", + "pyngsapi_c", + "pyngsrandom01_c", + "pyngsaccumulator_c", +): + _extension = getattr(cxx, _extension_name) + globals()[_extension_name] = _extension + sys.modules[__name__ + "." + _extension_name] = _extension # Optional solver modules are present when the wheel was built from an ALPS # checkout with application bindings enabled. -try: - from ._ext import cthyb, ctint - sys.modules[__name__ + ".cthyb"] = cthyb - sys.modules[__name__ + ".ctint"] = ctint -except ImportError: - pass +for _extension_name in ("maxent_c", "cthyb", "ctint"): + try: + _extension = __import__( + __name__ + "._ext." + _extension_name, fromlist=[_extension_name] + ) + except ImportError: + continue + globals()[_extension_name] = _extension + sys.modules[__name__ + "." + _extension_name] = _extension + +del _extension_name, _extension diff --git a/bindings/python/pyalps/src/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py index 72d4ed87b..328c7904c 100644 --- a/bindings/python/pyalps/src/pyalps/mpi.py +++ b/bindings/python/pyalps/src/pyalps/mpi.py @@ -1,23 +1,358 @@ -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2012 by Matthias Troyer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - -# The Boost.Python-era mpi_c extension is not part of the nanobind -# wheel build (no target builds it), so the old fallback chain -# (.cxx.mpi_c → mpi_c → boost.mpi) could never succeed anyway. Fail -# with an explanation instead of a misleading "No module named -# 'boost'". -raise ImportError( - "pyalps.mpi is not available: the MPI bindings were not ported to the " - "nanobind build of pyalps. Drive MPI-parallel simulations from C++, or " - "use mpi4py for Python-side MPI communication." -) +"""MPI compatibility layer backed by :mod:`mpi4py`. + +The historic module re-exported Boost.MPI's Boost.Python bindings. Rebuilding +that second Python binding stack would couple pyalps to Boost.Python again and +make ordinary wheels depend on one particular MPI implementation. Instead, +this module preserves the commonly used Boost.MPI Python spelling on top of +mpi4py. Install ``pyalps[mpi]`` to enable it. + +The compatibility surface covers the world communicator, point-to-point +operations, collectives, status/request types, environment queries, and the +timer API. It intentionally does not reproduce Boost.MPI's C++/Python +serialization bridge or skeleton/content optimization; use mpi4py buffers for +that level of interoperability. +""" + +from __future__ import annotations + +import atexit as _atexit +from functools import reduce as _python_reduce +import sys +from typing import Any + +try: + import mpi4py as _mpi4py + + _mpi_module_was_loaded = "mpi4py.MPI" in sys.modules + _previous_auto_initialize = _mpi4py.rc.initialize + if not _mpi_module_was_loaded: + # Delay mpi4py's automatic MPI_Init just long enough to distinguish an + # externally initialized MPI process from an environment this module + # must own. This reproduces Boost.MPI's finalize-only-what-we-created + # behavior and leaves the global mpi4py setting as we found it. + _mpi4py.rc.initialize = False + try: + from mpi4py import MPI as _MPI + finally: + if not _mpi_module_was_loaded: + _mpi4py.rc.initialize = _previous_auto_initialize +except ImportError as error: # pragma: no cover - depends on optional install + raise ImportError( + "pyalps.mpi requires mpi4py; install the optional dependency with " + "'python -m pip install pyalps[mpi]'" + ) from error + + +_initialized_here = False +if not _MPI.Is_initialized(): + _MPI.Init() + _initialized_here = True + + +any_source = _MPI.ANY_SOURCE +any_tag = _MPI.ANY_TAG +Exception = _MPI.Exception +Status = _MPI.Status + + +class Request: + """Non-value request with the Boost.MPI ``wait``/``test`` contract.""" + + def __init__(self, request: Any): + self._request = request + + def wait(self): + status = Status() + self._request.wait(status) + return status + + def test(self): + status = Status() + flag, _value = self._request.test(status) + return status if flag else None + + def cancel(self) -> None: + self._request.cancel() + + +class RequestWithValue(Request): + """Receive request whose completion returns ``(value, status)``.""" + + def wait(self): + status = Status() + value = self._request.wait(status) + return value, status + + def test(self): + status = Status() + flag, value = self._request.test(status) + return (value, status) if flag else None + + +class RequestList(list): + """Mutable request sequence used by the nonblocking helper functions.""" + + +class Communicator: + """Boost.MPI-compatible wrapper around an ``mpi4py.MPI.Comm``.""" + + def __init__(self, comm: Any = None): + if isinstance(comm, Communicator): + comm = comm._comm + self._comm = _MPI.COMM_WORLD if comm is None else comm + + @property + def rank(self) -> int: + return self._comm.rank + + @property + def size(self) -> int: + return self._comm.size + + def __bool__(self) -> bool: + return self._comm != _MPI.COMM_NULL + + def __eq__(self, other: object) -> bool: + return isinstance(other, Communicator) and self._comm == other._comm + + def send(self, dest: int, tag: int = 0, value: Any = None) -> None: + self._comm.send(value, dest=dest, tag=tag) + + def recv( + self, + source: int = any_source, + tag: int = any_tag, + return_status: bool = False, + ) -> Any: + status = _MPI.Status() if return_status else None + value = self._comm.recv(source=source, tag=tag, status=status) + return (value, status) if return_status else value + + def isend(self, dest: int, tag: int = 0, value: Any = None): + return Request(self._comm.isend(value, dest=dest, tag=tag)) + + def irecv(self, source: int = any_source, tag: int = any_tag): + return RequestWithValue(self._comm.irecv(source=source, tag=tag)) + + def probe(self, source: int = any_source, tag: int = any_tag): + status = _MPI.Status() + self._comm.probe(source=source, tag=tag, status=status) + return status + + def iprobe(self, source: int = any_source, tag: int = any_tag): + status = _MPI.Status() + return status if self._comm.iprobe(source=source, tag=tag, status=status) else None + + def barrier(self) -> None: + self._comm.barrier() + + def split(self, color: int, key: int = 0) -> "Communicator": + return Communicator(self._comm.Split(color=color, key=key)) + + def abort(self, errcode: int) -> None: + self._comm.Abort(errcode) + + +world = Communicator(_MPI.COMM_WORLD) +rank = world.rank +size = world.size + + +def _unwrap(comm: Any): + return comm._comm if isinstance(comm, Communicator) else comm + + +def _collective_values(comm: Any, value: Any) -> tuple[Any, ...]: + return tuple(_unwrap(comm).allgather(value)) + + +def all_gather(comm: Any = world, value: Any = None) -> tuple[Any, ...]: + return _collective_values(comm, value) + + +def all_to_all(comm: Any = world, values: Any = None) -> tuple[Any, ...]: + return tuple(_unwrap(comm).alltoall(values)) + + +def broadcast(comm: Any = world, value: Any = None, root: int = 0) -> Any: + return _unwrap(comm).bcast(value, root=root) + + +def gather(comm: Any = world, value: Any = None, root: int = 0): + values = _unwrap(comm).gather(value, root=root) + return tuple(values) if _unwrap(comm).rank == root else None + + +def scatter(comm: Any = world, values: Any = None, root: int = 0) -> Any: + return _unwrap(comm).scatter(values, root=root) + + +def _apply_operation(values: tuple[Any, ...], op: Any) -> Any: + if op is None: + raise TypeError("an operation callable is required") + return _python_reduce(op, values) + + +def reduce(comm: Any = world, value: Any = None, op: Any = None, root: int = 0): + # Boost.MPI accepted arbitrary Python callables. Gathering before the + # Python reduction preserves that behavior; users wanting native MPI + # reductions can call the underlying ``world._comm`` directly. + values = gather(comm, value, root) + return _apply_operation(values, op) if _unwrap(comm).rank == root else None + + +def all_reduce(comm: Any = world, value: Any = None, op: Any = None) -> Any: + return _apply_operation(_collective_values(comm, value), op) + + +def scan(comm: Any = world, value: Any = None, op: Any = None) -> Any: + values = _collective_values(comm, value) + return _apply_operation(values[: _unwrap(comm).rank + 1], op) + + +def _check_requests(requests) -> None: + if not requests: + raise ValueError("cannot wait on an empty request vector") + if not all(isinstance(request, Request) for request in requests): + raise TypeError("requests must contain pyalps.mpi Request objects") + + +def _raw_requests(requests): + _check_requests(requests) + return [request._request for request in requests] + + +def wait_any(requests): + status = Status() + index, value = _MPI.Request.waitany(_raw_requests(requests), status) + return value, status, index + + +def test_any(requests): + status = Status() + index, flag, value = _MPI.Request.testany(_raw_requests(requests), status) + return (value, status, index) if flag else None + + +def wait_all(requests, callable=None) -> None: + statuses = [Status() for _ in requests] + values = _MPI.Request.waitall(_raw_requests(requests), statuses) + if callable is not None: + for value, status in zip(values, statuses): + callable(value, status) + + +def test_all(requests, callable=None) -> bool: + statuses = [Status() for _ in requests] + flag, values = _MPI.Request.testall(_raw_requests(requests), statuses) + if flag and callable is not None and values is not None: + for value, status in zip(values, statuses): + callable(value, status) + return bool(flag) + + +def wait_some(requests, callable=None) -> int: + statuses = [Status() for _ in requests] + indices, values = _MPI.Request.waitsome(_raw_requests(requests), statuses) + return _finish_some(requests, indices, values, statuses, callable) + + +def test_some(requests, callable=None) -> int: + statuses = [Status() for _ in requests] + indices, values = _MPI.Request.testsome(_raw_requests(requests), statuses) + return _finish_some(requests, indices, values, statuses, callable) + + +def _finish_some(requests, indices, values, statuses, callable) -> int: + if not indices: + return len(requests) + if callable is not None: + for value, status in zip(values, statuses): + callable(value, status) + + # Boost.MPI partitions the mutable RequestList into pending requests + # followed by completed requests and returns the first completed index. + completed = set(indices) + pending_requests = [r for i, r in enumerate(requests) if i not in completed] + completed_requests = [requests[i] for i in indices] + requests[:] = pending_requests + completed_requests + return len(pending_requests) + + +class Timer: + def __init__(self): + self.restart() + + def restart(self) -> float: + previous = getattr(self, "_start", _MPI.Wtime()) + self._start = _MPI.Wtime() + return self._start - previous + + @property + def elapsed(self) -> float: + return _MPI.Wtime() - self._start + + @property + def elapsed_min(self) -> float: + return _MPI.Wtick() + + @property + def elapsed_max(self) -> float: + return sys.float_info.max + + @property + def time_is_global(self) -> bool: + return bool(_MPI.COMM_WORLD.Get_attr(_MPI.WTIME_IS_GLOBAL)) + + +def init(argv=None, abort_on_exception: bool = True) -> bool: + del argv, abort_on_exception + global _initialized_here + if _MPI.Is_initialized(): + return False + _MPI.Init() + _initialized_here = True + return True + + +def finalize() -> None: + global _initialized_here + if _initialized_here and _MPI.Is_initialized() and not _MPI.Is_finalized(): + _MPI.Finalize() + _initialized_here = False + + +if _initialized_here: + _atexit.register(finalize) + + +def abort(errcode: int) -> None: + _MPI.COMM_WORLD.Abort(errcode) + + +def initialized() -> bool: + return _MPI.Is_initialized() + + +def finalized() -> bool: + return _MPI.Is_finalized() + + +collectives_tag = _MPI.COMM_WORLD.Get_attr(_MPI.TAG_UB) +max_tag = collectives_tag - 1 +processor_name = _MPI.Get_processor_name() +_host_key = getattr(_MPI, "HOST", None) +_io_key = getattr(_MPI, "IO", None) +host_rank = _MPI.COMM_WORLD.Get_attr(_host_key) if _host_key is not None else None +io_rank = _MPI.COMM_WORLD.Get_attr(_io_key) if _io_key is not None else None + + +__all__ = [ + "Communicator", "Exception", "Request", "RequestList", "RequestWithValue", + "Status", "Timer", "abort", "all_gather", "all_reduce", "all_to_all", + "any_source", "any_tag", "broadcast", "collectives_tag", "finalize", + "finalized", "gather", "host_rank", "init", "initialized", "io_rank", + "max_tag", "processor_name", "rank", "reduce", "scan", "scatter", "size", + "test_all", "test_any", "test_some", "wait_all", "wait_any", "wait_some", + "world", +] diff --git a/src/alps/ngs/detail/export_sim_to_python.hpp b/src/alps/ngs/detail/export_sim_to_python.hpp new file mode 100644 index 000000000..b5dcb2b92 --- /dev/null +++ b/src/alps/ngs/detail/export_sim_to_python.hpp @@ -0,0 +1,107 @@ +// Copyright (C) 2010-2012 by Lukas Gamper +// 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT +// +// Header-only nanobind support for downstream ALPS simulations. Keeping this +// integration in an opt-in header prevents libalps itself from depending on +// Python or nanobind while preserving the historic public include path and +// ALPS_EXPORT_SIM_TO_PYTHON entry point. +#ifndef ALPS_NGS_DETAIL_EXPORT_SIM_TO_PYTHON_HPP +#define ALPS_NGS_DETAIL_EXPORT_SIM_TO_PYTHON_HPP + +#include +#include + +#include +#include +#include + +#include +#include + +namespace alps { +namespace python { + +namespace nb = nanobind; + +template +class exported_simulation : public Simulation { +public: + using parameters_type = typename Simulation::parameters_type; + using result_names_type = typename Simulation::result_names_type; + using results_type = typename Simulation::results_type; + + explicit exported_simulation(parameters_type const & parameters, + std::size_t seed_offset = 0) + : Simulation(parameters, seed_offset) {} + + // mcbase predates virtual-destructor guidance. The Python-owned concrete + // wrapper is nevertheless polymorphic, so give this boundary type its own + // virtual destructor and ensure nanobind always destroys the full object. + virtual ~exported_simulation() = default; + + bool run_python(nb::object stop_callback) { + return Simulation::run([stop_callback]() -> bool { + nb::gil_scoped_acquire gil; + return nb::cast(stop_callback()); + }); + } + + results_type collect_results_python( + result_names_type const & names = result_names_type()) const { + return names.empty() ? Simulation::collect_results() + : Simulation::collect_results(names); + } + + alps::random01 & get_random() { return this->random; } + parameters_type & get_parameters() { return this->parameters; } + auto & get_measurements() { + return this->measurements; + } +}; + +template +void export_sim_to_python(nb::module_ & module, char const * name) { + // nanobind's type registry is shared across extension modules. Import the + // owning pyalps modules before declaring a derived simulation so mcbase, + // params, archive, result and observable types are already registered. + nb::module_::import_("pyalps.ngs"); + nb::module_::import_("pyalps.hdf5"); + + using wrapper = exported_simulation; + nb::class_(module, name) + .def(nb::init(), + nb::arg("parameters"), nb::arg("seed_offset") = 0) + .def_prop_ro("random", &wrapper::get_random, + nb::rv_policy::reference_internal) + .def_prop_ro("parameters", &wrapper::get_parameters, + nb::rv_policy::reference_internal) + .def_prop_ro("measurements", &wrapper::get_measurements, + nb::rv_policy::reference_internal) + .def("run", &wrapper::run_python, nb::arg("stop_callback")) + .def("resultNames", &wrapper::result_names) + .def("unsavedResultNames", &wrapper::unsaved_result_names) + .def("collectResults", &wrapper::collect_results_python, + nb::arg("names") = typename wrapper::result_names_type()) + .def("save", + [](wrapper const & self, alps::hdf5::archive & archive) { + static_cast(self).save(archive); + }) + .def("load", + [](wrapper & self, alps::hdf5::archive & archive) { + static_cast(self).load(archive); + }); +} + +} // namespace python +} // namespace alps + +#define ALPS_NANOBIND_EXPORT_SIM_TO_PYTHON(MODULE, NAME, CLASS) \ + ::alps::python::export_sim_to_python((MODULE), #NAME) + +// Source-compatible spelling for old export.cpp files after changing their +// module declaration to ``NB_MODULE(module_name, m)``. +#define ALPS_EXPORT_SIM_TO_PYTHON(NAME, CLASS) \ + ALPS_NANOBIND_EXPORT_SIM_TO_PYTHON(m, NAME, CLASS) + +#endif diff --git a/src/alps/ngs/detail/paramproxy.hpp b/src/alps/ngs/detail/paramproxy.hpp index 972a41a2b..d98e45831 100644 --- a/src/alps/ngs/detail/paramproxy.hpp +++ b/src/alps/ngs/detail/paramproxy.hpp @@ -115,7 +115,10 @@ namespace alps { #define ALPS_NGS_PARAMPROXY_ADD_OPERATOR_DECL(T) \ ALPS_DECL T operator+(paramproxy const & p, T s); \ ALPS_DECL T operator+(T s, paramproxy const & p); - ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(ALPS_NGS_PARAMPROXY_ADD_OPERATOR_DECL) + // vector is a native stored parameter type, but unlike the + // historic numeric/string alternatives it has no meaningful or + // portable element-wise operator+=. + ALPS_NGS_FOREACH_PARAMETERVALUE_ADDABLE_TYPE(ALPS_NGS_PARAMPROXY_ADD_OPERATOR_DECL) #undef ALPS_NGS_PARAMPROXY_ADD_OPERATOR_DECL ALPS_DECL std::string operator+(paramproxy const & p, char const * s); diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index 8fef1a643..c17b95400 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -33,7 +33,7 @@ #include #include -#define ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE_NO_PYTHON(CALLBACK) \ +#define ALPS_NGS_FOREACH_PARAMETERVALUE_ADDABLE_TYPE(CALLBACK) \ CALLBACK(double) \ CALLBACK(int) \ CALLBACK(bool) \ @@ -44,6 +44,10 @@ CALLBACK(std::vector) \ CALLBACK(std::vector >) +#define ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE_NO_PYTHON(CALLBACK) \ + ALPS_NGS_FOREACH_PARAMETERVALUE_ADDABLE_TYPE(CALLBACK) \ + CALLBACK(std::vector) + #define ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(CALLBACK) \ ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE_NO_PYTHON(CALLBACK) @@ -79,6 +83,9 @@ namespace alps { template <> struct paramvalue_index > > { enum { value = 8 }; }; + template <> struct paramvalue_index > { + enum { value = 9 }; + }; class paramvalue; diff --git a/src/alps/ngs/detail/paramvalue_reader.hpp b/src/alps/ngs/detail/paramvalue_reader.hpp index eb93bf5a1..5ff38dbd1 100644 --- a/src/alps/ngs/detail/paramvalue_reader.hpp +++ b/src/alps/ngs/detail/paramvalue_reader.hpp @@ -44,7 +44,7 @@ namespace alps { template void operator()(U * const ptr, std::vector size) { if (size.size() != 1) throw std::invalid_argument("only 1 D array are supported in alps::params" + ALPS_STACKTRACE); - else + else if (size[0] != 0) for (U const * it = ptr; it != ptr + size[0]; ++it) (*this)(*it); } @@ -61,7 +61,7 @@ namespace alps { template void operator()(U * const ptr, std::vector size) { if (size.size() != 1) throw std::invalid_argument("only 1 D array are supported in alps::params" + ALPS_STACKTRACE); - else + else if (size[0] != 0) for (U const * it = ptr; it != ptr + size[0]; ++it) value += (it == ptr ? "," : "") + cast(*it); } @@ -79,11 +79,18 @@ namespace alps { } template void operator()(std::vector const & v) const { - visitor(&v.front(), std::vector(1, v.size())); + visitor(v.data(), std::vector(1, v.size())); } - void operator()(T const & v) const { - visitor.value = v; + // std::vector stores proxy bits rather than contiguous + // bool objects and therefore has no usable data() pointer. + // Materialise byte values for the existing conversion visitor; + // scalar targets still reject vector input, while vector targets + // convert each byte to their requested element type. + void operator()(std::vector const & v) const { + std::vector contiguous(v.begin(), v.end()); + visitor(contiguous.data(), + std::vector(1, contiguous.size())); } T const & get_value() { diff --git a/src/alps/ngs/lib/paramproxy.cpp b/src/alps/ngs/lib/paramproxy.cpp index 4bb00adbb..9e4dd25ab 100644 --- a/src/alps/ngs/lib/paramproxy.cpp +++ b/src/alps/ngs/lib/paramproxy.cpp @@ -57,7 +57,7 @@ namespace alps { using boost::numeric::operators::operator+=; \ return s += p.cast< T >(); \ } - ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(ALPS_NGS_PARAMPROXY_ADD_OPERATOR_IMPL) + ALPS_NGS_FOREACH_PARAMETERVALUE_ADDABLE_TYPE(ALPS_NGS_PARAMPROXY_ADD_OPERATOR_IMPL) #undef ALPS_NGS_PARAMPROXY_ADD_OPERATOR_IMPL std::string operator+(paramproxy const & p, char const * s) { diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index dc9c7faa1..22373a0c5 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -95,6 +95,7 @@ namespace alps { ) ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK(double, std::vector) ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK(int, std::vector) + ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK(bool, std::vector) ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK( std::string, std::vector ) diff --git a/src/alps/python/make_copy.hpp b/src/alps/python/make_copy.hpp new file mode 100644 index 000000000..4ba2e1a4a --- /dev/null +++ b/src/alps/python/make_copy.hpp @@ -0,0 +1,20 @@ +// Copyright (C) 2010 by Matthias Troyer +// 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT +#ifndef ALPS_PYTHON_MAKE_COPY_HPP +#define ALPS_PYTHON_MAKE_COPY_HPP + +#include + +namespace alps { +namespace python { + +template +T make_copy(T const & value, nanobind::handle /*memo*/) { + return value; +} + +} // namespace python +} // namespace alps + +#endif diff --git a/src/alps/python/save_observable_to_hdf5.hpp b/src/alps/python/save_observable_to_hdf5.hpp new file mode 100644 index 000000000..b29a5fe27 --- /dev/null +++ b/src/alps/python/save_observable_to_hdf5.hpp @@ -0,0 +1,25 @@ +// Copyright (C) 2010 by Matthias Troyer +// SPDX-License-Identifier: MIT +#ifndef ALPS_PYTHON_SAVE_OBSERVABLE_TO_HDF5_HPP +#define ALPS_PYTHON_SAVE_OBSERVABLE_TO_HDF5_HPP + +#include + +#include + +namespace alps { +namespace python { + +// Despite its historic namespace this helper is ordinary typed C++ and has no +// dependency on Python. Retain it for downstream source compatibility. +template +void save_observable_to_hdf5(Observable const & observable, + std::string const & filename) { + hdf5::archive archive(filename, "a"); + archive["/simulation/results/" + observable.representation()] << observable; +} + +} // namespace python +} // namespace alps + +#endif diff --git a/test/ngs/params/assign.cpp b/test/ngs/params/assign.cpp index 7e933d259..2c0336842 100644 --- a/test/ngs/params/assign.cpp +++ b/test/ngs/params/assign.cpp @@ -14,6 +14,9 @@ #include +#include +#include + int main() { alps::params parms; @@ -32,8 +35,12 @@ int main() { parms["double"] = static_cast(1); parms["long double"] = static_cast(1); parms["bool"] = static_cast(1); + std::vector const bool_vector{true, false, true}; + parms["std::vector"] = bool_vector; parms["std::string"] = std::string("asdf"); + assert(parms["std::vector"].cast >() == bool_vector); + std::cout << parms << std::endl; return 0; } diff --git a/test/pyalps/pyhdf5io_test.py b/test/pyalps/pyhdf5io_test.py index 5e21e81d3..17833afa2 100644 --- a/test/pyalps/pyhdf5io_test.py +++ b/test/pyalps/pyhdf5io_test.py @@ -306,9 +306,104 @@ def test_hdf5_nested_numpy_scalar_vectorization(): del ar +def test_hdf5_zero_dimensional_and_zero_extent_arrays(): + scalar_cases = [ + np.array(True), + np.array(-3, dtype=np.int32), + np.array(2**40, dtype=np.int64), + np.array(1.25, dtype=np.float32), + np.array(1.25, dtype=np.float64), + np.array(1 + 2j, dtype=np.complex64), + np.array(1 + 2j, dtype=np.complex128), + ] + empty_cases = [ + np.empty((0,), dtype=np.float64), + np.empty((0, 2), dtype=np.int32), + np.empty((2, 0), dtype=np.int32), + np.empty((2, 0, 3), dtype=np.complex128), + ] + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "zero-shapes.h5") + with hdf5.archive(path, "w") as ar: + for index, value in enumerate(scalar_cases): + ar[f"/scalar/{index}"] = value + for index, value in enumerate(empty_cases): + ar[f"/empty/{index}"] = value + + with hdf5.archive(path, "r") as ar: + for index, expected in enumerate(scalar_cases): + actual = ar[f"/scalar/{index}"] + assert np.asarray(actual).shape == () + assert actual == expected.item() + if expected.dtype == np.complex64: + assert np.asarray(actual).dtype == np.complex64 + for index, expected in enumerate(empty_cases): + actual = ar[f"/empty/{index}"] + assert isinstance(actual, np.ndarray) + assert actual.shape == expected.shape + assert actual.dtype == expected.dtype + + +def test_hdf5_complex_array_precision_roundtrip(): + values = [ + np.array([1 + 2j, 3 + 4j], dtype=np.complex64), + np.array([[1 + 2j], [3 + 4j]], dtype=np.complex64), + np.array([1 + 2j, 3 + 4j], dtype=np.complex128), + ] + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "complex-precision.h5") + with hdf5.archive(path, "w") as ar: + for index, value in enumerate(values): + ar[f"/{index}"] = value + with hdf5.archive(path, "r") as ar: + for index, expected in enumerate(values): + actual = ar[f"/{index}"] + assert actual.dtype == expected.dtype + np.testing.assert_array_equal(actual, expected) + + +def test_hdf5_strided_array_roundtrip(): + base = np.arange(24, dtype=np.float64).reshape(4, 6) + values = [ + base[:, ::2], + base.T, + base[::-1, ::-2], + np.ma.array(base[:, ::2], mask=False), + (base.astype(np.complex64) * (1 + 2j))[::2, 1::2], + ] + assert all(not value.flags.c_contiguous for value in values) + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "strided.h5") + with hdf5.archive(path, "w") as ar: + for index, value in enumerate(values): + ar[f"/{index}"] = value + with hdf5.archive(path, "r") as ar: + for index, expected in enumerate(values): + actual = ar[f"/{index}"] + assert actual.dtype == expected.dtype + np.testing.assert_array_equal(actual, expected) + + +def test_hdf5_non_native_array_error_is_actionable(): + value = np.arange(4, dtype=np.int32).byteswap().view(np.dtype(">i4")) + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "non-native.h5") + with hdf5.archive(path, "w") as ar: + try: + ar["/value"] = value + raise AssertionError("non-native arrays must be rejected") + except RuntimeError as error: + assert "not native" in str(error) + + if __name__ == "__main__": test_hdf5io() test_hdf5_empty_dict_roundtrip() test_hdf5_dict_key_roundtrip() test_hdf5_nested_numpy_scalar_vectorization() + test_hdf5_zero_dimensional_and_zero_extent_arrays() + test_hdf5_complex_array_precision_roundtrip() + test_hdf5_strided_array_roundtrip() + test_hdf5_non_native_array_error_is_actionable() print("SUCCESS") diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 2ced13624..572f1486b 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -9,10 +9,15 @@ import copy import importlib import os +from pathlib import Path +import subprocess +import sys import tempfile +import time from types import SimpleNamespace import numpy as np +import pytest def test_extension_import_surface(): @@ -36,6 +41,10 @@ def test_extension_import_surface(): } assert pyalps is not None assert expected <= set(vars(cxx)) + for name in expected: + direct = importlib.import_module("pyalps." + name) + assert direct is getattr(cxx, name) + assert getattr(pyalps, name) is direct def test_cross_module_parameter_archive_and_rng_roundtrip(): @@ -216,6 +225,274 @@ def test_optional_application_extension_surface(): for name in ("maxent_c", "dwa_c", "cthyb", "ctint"): module = importlib.import_module("pyalps._ext." + name) assert module.__name__.endswith(name) + assert importlib.import_module("pyalps." + name) is module + + from pyalps import cthyb, ctint, maxent_c + assert callable(maxent_c.AnalyticContinuation) + assert callable(cthyb.solve) + assert callable(ctint.solve) + + from pyalps._ext import dwa_c + + worldlines = dwa_c.worldlines(3) + assert worldlines.states() == [0, 0, 0] + assert dwa_c.std_vector_double([1.0, 2.0]) == [1.0, 2.0] + assert isinstance(worldlines.states(), dwa_c.std_vector_unsigned_short) + bands = dwa_c.bandstructure([1.0], [2.0], 1.0, 1.0, 1) + assert len(bands.t()) == 3 + + +def test_mpi4py_compatibility_surface(): + pytest.importorskip("mpi4py") + import operator + from pyalps import ngs + import pyalps.mpi as mpi + + assert mpi.initialized() + assert mpi.world.rank == mpi.rank + assert mpi.world.size == mpi.size + assert issubclass(mpi.Exception, Exception) + assert mpi.Communicator().rank == mpi.rank + assert mpi.broadcast(value={"rank": mpi.rank}, root=0) == {"rank": 0} + assert mpi.all_gather(value=mpi.rank) == tuple(range(mpi.size)) + gathered = mpi.gather(value=mpi.rank, root=0) + if mpi.rank == 0: + assert gathered == tuple(range(mpi.size)) + else: + assert gathered is None + scattered = mpi.scatter( + values=tuple("rank-{}".format(index) for index in range(mpi.size)) + if mpi.rank == 0 else None, + root=0, + ) + assert scattered == "rank-{}".format(mpi.rank) + exchanged = mpi.all_to_all( + values=tuple((mpi.rank, destination) for destination in range(mpi.size)) + ) + assert exchanged == tuple((source, mpi.rank) for source in range(mpi.size)) + assert mpi.reduce(value=1, op=operator.add, root=0) == ( + mpi.size if mpi.rank == 0 else None + ) + assert mpi.all_reduce(value=1, op=operator.add) == mpi.size + assert mpi.scan(value=mpi.rank + 1, op=operator.add) == ( + (mpi.rank + 1) * (mpi.rank + 2) // 2 + ) + + subgroup = mpi.world.split(color=mpi.rank % 2, key=mpi.rank) + assert subgroup and subgroup.rank >= 0 and subgroup.size >= 1 + mpi.world.barrier() + + # Exercise actual inter-rank transport under mpiexec, while remaining a + # valid self-send in the ordinary one-process wheel test. + send_to = (mpi.rank + 1) % mpi.size + receive_from = (mpi.rank - 1) % mpi.size + ring_request = mpi.world.isend( + send_to, tag=172, value={"source": mpi.rank, "payload": "ring"} + ) + ring_value, ring_status = mpi.world.recv( + receive_from, tag=172, return_status=True + ) + ring_request.wait() + assert ring_value == {"source": receive_from, "payload": "ring"} + assert ring_status.source == receive_from and ring_status.tag == 172 + + # Point-to-point spelling and return_status match Boost.MPI's Python API. + request = mpi.world.isend(mpi.rank, tag=173, value="self") + value, status = mpi.world.recv(mpi.rank, tag=173, return_status=True) + send_status = request.wait() + assert value == "self" + assert status.source == mpi.rank and status.tag == 173 + assert isinstance(send_status, mpi.Status) + + send_request = mpi.world.isend(mpi.rank, tag=174, value="async") + receive_request = mpi.world.irecv(mpi.rank, tag=174) + assert isinstance(send_request, mpi.Request) + assert isinstance(receive_request, mpi.RequestWithValue) + received, receive_status = receive_request.wait() + send_request.wait() + assert received == "async" + assert receive_status.source == mpi.rank and receive_status.tag == 174 + + callbacks = [] + requests = mpi.RequestList([ + mpi.world.isend(mpi.rank, tag=175, value="batch"), + mpi.world.irecv(mpi.rank, tag=175), + ]) + mpi.wait_all(requests, lambda result, result_status: callbacks.append( + (result, result_status) + )) + assert callbacks[1][0] == "batch" + assert callbacks[1][1].source == mpi.rank + + any_send = mpi.world.isend(mpi.rank, tag=176, value="any") + any_requests = mpi.RequestList([mpi.world.irecv(mpi.rank, tag=176)]) + any_value, any_status, any_index = mpi.wait_any(any_requests) + any_send.wait() + assert (any_value, any_index) == ("any", 0) + assert any_status.source == mpi.rank + + some_callbacks = [] + some_send = mpi.world.isend(mpi.rank, tag=177, value="some") + some_requests = mpi.RequestList([mpi.world.irecv(mpi.rank, tag=177)]) + boundary = mpi.wait_some( + some_requests, + lambda result, result_status: some_callbacks.append( + (result, result_status.source) + ), + ) + some_send.wait() + assert boundary == 0 + assert some_callbacks == [("some", mpi.rank)] + + poll_send = mpi.world.isend(mpi.rank, tag=178, value="request-test") + poll_receive = mpi.world.irecv(mpi.rank, tag=178) + deadline = time.monotonic() + 5 + poll_result = None + while poll_result is None and time.monotonic() < deadline: + poll_result = poll_receive.test() + poll_send.wait() + assert poll_result is not None + assert poll_result[0] == "request-test" + + any_test_send = mpi.world.isend(mpi.rank, tag=179, value="test-any") + any_test_requests = mpi.RequestList([mpi.world.irecv(mpi.rank, tag=179)]) + deadline = time.monotonic() + 5 + any_test_result = None + while any_test_result is None and time.monotonic() < deadline: + any_test_result = mpi.test_any(any_test_requests) + any_test_send.wait() + assert any_test_result is not None + assert (any_test_result[0], any_test_result[2]) == ("test-any", 0) + + all_test_callbacks = [] + all_test_send = mpi.world.isend(mpi.rank, tag=180, value="test-all") + all_test_requests = mpi.RequestList([mpi.world.irecv(mpi.rank, tag=180)]) + deadline = time.monotonic() + 5 + while (not mpi.test_all( + all_test_requests, + lambda result, result_status: all_test_callbacks.append( + (result, result_status.source) + ), + ) and time.monotonic() < deadline): + pass + all_test_send.wait() + assert all_test_callbacks == [("test-all", mpi.rank)] + + some_test_callbacks = [] + some_test_send = mpi.world.isend(mpi.rank, tag=181, value="test-some") + some_test_requests = mpi.RequestList([mpi.world.irecv(mpi.rank, tag=181)]) + deadline = time.monotonic() + 5 + some_test_boundary = len(some_test_requests) + while some_test_boundary != 0 and time.monotonic() < deadline: + some_test_boundary = mpi.test_some( + some_test_requests, + lambda result, result_status: some_test_callbacks.append( + (result, result_status.source) + ), + ) + some_test_send.wait() + assert some_test_boundary == 0 + assert some_test_callbacks == [("test-some", mpi.rank)] + + probe_send = mpi.world.isend(mpi.rank, tag=182, value="probe") + probe_status = mpi.world.probe(mpi.rank, tag=182) + assert probe_status.source == mpi.rank and probe_status.tag == 182 + assert mpi.world.recv(mpi.rank, tag=182) == "probe" + probe_send.wait() + + iprobe_send = mpi.world.isend(mpi.rank, tag=183, value="iprobe") + deadline = time.monotonic() + 5 + iprobe_status = None + while iprobe_status is None and time.monotonic() < deadline: + iprobe_status = mpi.world.iprobe(mpi.rank, tag=183) + assert iprobe_status is not None + assert iprobe_status.source == mpi.rank and iprobe_status.tag == 183 + assert mpi.world.recv(mpi.rank, tag=183) == "iprobe" + iprobe_send.wait() + + timer = mpi.Timer() + assert timer.elapsed >= 0 + assert 0 < timer.elapsed_min < timer.elapsed_max + assert mpi.max_tag + 1 == mpi.collectives_tag + + # The legacy mcbase constructor accepted a communicator but did not use + # it internally. Preserve that call shape without binding Boost.MPI. + class Simulation(ngs.mcbase): + def update(self): + pass + + def measure(self): + pass + + def fraction_completed(self): + return 1.0 + + assert isinstance(Simulation({"SEED": 1}, 42, mpi.world), ngs.mcbase) + + +def test_mpi_finalization_ownership(): + pytest.importorskip("mpi4py") + + # Boost.MPI finalized only an environment its Python module initialized. + # Importing pyalps.mpi after an existing mpi4py user must therefore leave + # that user's MPI process alive when pyalps.mpi.finalize() is called. + externally_owned = """ +from mpi4py import MPI +import pyalps.mpi as mpi +assert not mpi._initialized_here +mpi.finalize() +assert MPI.Is_initialized() and not MPI.Is_finalized() +""" + subprocess.run([sys.executable, "-c", externally_owned], check=True) + + # Conversely, a direct pyalps.mpi import owns the initialization and its + # explicit finalize call must release it. + pyalps_owned = """ +import pyalps.mpi as mpi +assert mpi._initialized_here +mpi.finalize() +assert mpi.finalized() +""" + subprocess.run([sys.executable, "-c", pyalps_owned], check=True) + + +@pytest.mark.skipif( + os.environ.get("PYALPS_TEST_DOWNSTREAM_EXPORT") != "1", + reason="enabled for one wheel per platform in packaging CI", +) +def test_downstream_nanobind_simulation_export(tmp_path): + """Build and run a consumer extension against the installed ALPS SDK.""" + repository = Path(__file__).resolve().parents[2] + tutorial = repository / "tutorials" / "ngs" / "5_export_python" + alps_dir = repository / "_build" / "wheel-deps" / "install" / "share" / "alps" + build = tmp_path / "export-python-build" + + assert (alps_dir / "ALPSConfig.cmake").is_file() + subprocess.run( + [ + "cmake", "-S", str(tutorial), "-B", str(build), + "-DALPS_DIR={}".format(alps_dir), + "-DPython_EXECUTABLE={}".format(sys.executable), + ], + check=True, + ) + subprocess.run( + ["cmake", "--build", str(build), "--parallel", "2"], + check=True, + ) + + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + filter(None, (str(build), environment.get("PYTHONPATH"))) + ) + completed = subprocess.run( + [sys.executable, str(tutorial / "smoke_test.py")], + check=True, + capture_output=True, + env=environment, + text=True, + ) + assert "downstream nanobind export: ok" in completed.stdout def test_current_python_numpy_and_scipy_compatibility(monkeypatch): @@ -278,22 +555,63 @@ def test_params_mapping_equality_and_value_ladder(): raise AssertionError("[2**53+1] must be rejected") except TypeError as error: assert "32-bit" in str(error) - # bools (numpy bools included) never coerce to numbers - for bad in ([True, False], [np.bool_(True)]): - try: - p["flags"] = bad - raise AssertionError("bool list must be rejected") - except TypeError: - pass + # Homogeneous bool sequences have a native C++ representation and + # round-trip without falling back to stored Python objects. + p["flags"] = [True, False] + assert p["flags"] == [True, False] + p["npflags"] = np.array([True, False], dtype=np.bool_) + assert p["npflags"] == [True, False] + try: + p["mixedflags"] = [True, 1] + raise AssertionError("mixed bool/numeric sequences must be rejected") + except TypeError as error: + assert "cannot be mixed" in str(error) # numpy integer scalars are accepted like numpy floats are — # as scalars and inside lists, with the same 32-bit range policy p["npint"] = np.int64(8) assert p["npint"] == 8 and type(p["npint"]) is int p["npbool"] = np.bool_(True) assert p["npbool"] is True + p["npfloat32"] = np.float32(1.25) + assert p["npfloat32"] == 1.25 + p["nplongdouble"] = np.longdouble("1.125") + assert p["nplongdouble"] == 1.125 + p["npcomplex64"] = np.complex64(1 + 2j) + assert p["npcomplex64"] == 1 + 2j + p["npclongdouble"] = np.clongdouble(3 + 4j) + assert p["npclongdouble"] == 3 + 4j + p["npbytes"] = np.bytes_(b"native") + assert p["npbytes"] == "native" p["npints"] = [np.int64(1), np.int64(2)] assert p["npints"] == [1, 2] assert all(type(v) is int for v in p["npints"]) + p["nparray"] = np.array([1, 2], dtype=np.int64) + assert p["nparray"] == [1, 2] + p["npsubclass"] = np.ma.array([1, 2], mask=False) + assert p["npsubclass"] == [1, 2] + p["npfloats"] = np.array([1.5, 2.5], dtype=np.float32) + assert p["npfloats"] == [1.5, 2.5] + p["npcomplex"] = np.array([1 + 2j, 3 + 4j], dtype=np.complex64) + assert p["npcomplex"] == [1 + 2j, 3 + 4j] + p["npextendedcomplex"] = np.array( + [1 + 2j, 3 + 4j], dtype=np.clongdouble + ) + assert p["npextendedcomplex"] == [1 + 2j, 3 + 4j] + p["npcomplexlist"] = [np.complex64(5 + 6j), np.clongdouble(7 + 8j)] + assert p["npcomplexlist"] == [5 + 6j, 7 + 8j] + p["npstrings"] = np.array(["a", "b"]) + assert p["npstrings"] == ["a", "b"] + p["npbytestrings"] = np.array([b"a", b"b"], dtype="S1") + assert p["npbytestrings"] == ["a", "b"] + p["np0d"] = np.array(7, dtype=np.int64) + assert p["np0d"] == 7 + p["emptyflags"] = np.array([], dtype=np.bool_) + assert p["emptyflags"] == [] + try: + p["matrix"] = np.ones((2, 2)) + raise AssertionError("multidimensional parameter arrays must be rejected") + except TypeError as error: + assert "multidimensional" in str(error) try: p["npbig"] = [np.int64(2 ** 40)] raise AssertionError("[np.int64(2**40)] must be rejected") @@ -313,6 +631,27 @@ def test_params_mapping_equality_and_value_ladder(): p["cplx"] = 1 + 2j assert p["cplx"] == 1 + 2j + # Unsupported object graphs stay unsupported: params owns only native + # C++ values and must never keep arbitrary Python objects alive. + for unsupported in ({"nested": 1}, object()): + try: + p["object"] = unsupported + raise AssertionError("arbitrary Python objects must be rejected") + except TypeError: + pass + + +def test_params_native_bool_vector_hdf5_roundtrip(tmp_path): + from pyalps import hdf5, ngs + + filename = str(tmp_path / "bool-params.h5") + original = ngs.params({"flags": [True, False, True]}) + with hdf5.archive(filename, "w") as archive: + original.save(archive) + with hdf5.archive(filename, "r") as archive: + loaded = ngs.params(archive, "/") + assert loaded["flags"] == [True, False, True] + def test_observable_lshift_chains(): from pyalps import ngs diff --git a/tutorials/ngs/5_export_python/CMakeLists.txt b/tutorials/ngs/5_export_python/CMakeLists.txt new file mode 100644 index 000000000..8d6f954fd --- /dev/null +++ b/tutorials/ngs/5_export_python/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.22) +project(alps_nanobind_export_example LANGUAGES CXX) + +find_package(ALPS REQUIRED CONFIG) +find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) + +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_VARIABLE _nanobind_cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) +list(PREPEND CMAKE_PREFIX_PATH "${_nanobind_cmake_dir}") +find_package(nanobind 2.10 CONFIG REQUIRED) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(_alps_libraries ${ALPS_LIBRARIES}) +list(TRANSFORM _alps_libraries REPLACE "^hdf5-shared$" "hdf5") +separate_arguments(_alps_link_options NATIVE_COMMAND "${ALPS_EXTRA_LINKER_FLAGS}") +separate_arguments(_alps_compile_options NATIVE_COMMAND "${ALPS_CMAKE_CXX_FLAGS}") + +link_directories(${ALPS_LIBRARY_DIRS}) +set(_alps_runtime_paths ${ALPS_LIBRARY_DIRS}) +if(ALPS_HDF5_INCLUDE_DIR) + get_filename_component(_hdf5_prefix "${ALPS_HDF5_INCLUDE_DIR}" DIRECTORY) + link_directories("${_hdf5_prefix}/lib") + list(APPEND _alps_runtime_paths "${_hdf5_prefix}/lib") +endif() + +nanobind_add_module(ising_c NB_STATIC export2py.cpp ising.cpp) +target_include_directories(ising_c PRIVATE + ${ALPS_INCLUDE_DIRS} + ${ALPS_EXTRA_INCLUDE_DIRS}) +target_compile_options(ising_c PRIVATE ${_alps_compile_options}) +target_compile_definitions(ising_c PRIVATE ${ALPS_EXTRA_DEFINITIONS}) +target_link_libraries(ising_c PRIVATE ${_alps_libraries}) +target_link_options(ising_c PRIVATE ${_alps_link_options}) +set_target_properties(ising_c PROPERTIES + INSTALL_RPATH "${_alps_runtime_paths}" + BUILD_RPATH "${_alps_runtime_paths}") diff --git a/tutorials/ngs/5_export_python/README.md b/tutorials/ngs/5_export_python/README.md new file mode 100644 index 000000000..258aa1e68 --- /dev/null +++ b/tutorials/ngs/5_export_python/README.md @@ -0,0 +1,20 @@ +# Export an ALPS simulation with nanobind + +This example replaces the former Boost.Python export tutorial while retaining +the public `ALPS_EXPORT_SIM_TO_PYTHON` helper. Build it against an installed +ALPS SDK and the Python environment containing pyalps and nanobind: + +```sh +cmake -S . -B build -GNinja \ + -DALPS_DIR=/path/to/alps/share/alps \ + -DPython_EXECUTABLE="$(command -v python)" +cmake --build build +PYTHONPATH="$PWD/build" python main.py +``` + +For an old export source, replace `BOOST_PYTHON_MODULE(name) {` with +`NB_MODULE(name, m) {`; the existing +`ALPS_EXPORT_SIM_TO_PYTHON(PythonName, SimulationClass)` call remains valid. +The helper imports pyalps' owning extension modules before registering the +derived class, so ALPS parameter, archive, observable, and result types are +shared safely through nanobind's process-wide type registry. diff --git a/tutorials/ngs/5_export_python/export2py.cpp b/tutorials/ngs/5_export_python/export2py.cpp new file mode 100644 index 000000000..273119281 --- /dev/null +++ b/tutorials/ngs/5_export_python/export2py.cpp @@ -0,0 +1,12 @@ +// Copyright (C) 2010-2012 by Lukas Gamper +// 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT + +#include "ising.hpp" + +#include +#include + +NB_MODULE(ising_c, m) { + ALPS_EXPORT_SIM_TO_PYTHON(sim, ising_sim); +} diff --git a/tutorials/ngs/5_export_python/ising.cpp b/tutorials/ngs/5_export_python/ising.cpp new file mode 100644 index 000000000..a477fb51e --- /dev/null +++ b/tutorials/ngs/5_export_python/ising.cpp @@ -0,0 +1,42 @@ +// Copyright (C) 2010-2012 by Lukas Gamper +// 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT + +#include "ising.hpp" + +#include +#include + +#include + +ising_sim::ising_sim(parameters_type const & parameters, + std::size_t seed_offset) + : alps::mcbase(parameters, seed_offset), + total_sweeps_(parameters["SWEEPS"] | 10) { + measurements << alps::accumulator::RealObservable("Magnetization"); +} + +void ising_sim::update() { + state_ = random() < 0.5 ? -1.0 : 1.0; + ++sweeps_; +} + +void ising_sim::measure() { + measurements["Magnetization"] << state_; +} + +double ising_sim::fraction_completed() const { + return std::min(1.0, static_cast(sweeps_) / total_sweeps_); +} + +void ising_sim::save(alps::hdf5::archive & archive) const { + alps::mcbase::save(archive); + archive["/checkpoint/sweeps"] << sweeps_; + archive["/checkpoint/state"] << state_; +} + +void ising_sim::load(alps::hdf5::archive & archive) { + alps::mcbase::load(archive); + archive["/checkpoint/sweeps"] >> sweeps_; + archive["/checkpoint/state"] >> state_; +} diff --git a/tutorials/ngs/5_export_python/ising.hpp b/tutorials/ngs/5_export_python/ising.hpp new file mode 100644 index 000000000..7f86ade33 --- /dev/null +++ b/tutorials/ngs/5_export_python/ising.hpp @@ -0,0 +1,28 @@ +// Copyright (C) 2010-2012 by Lukas Gamper +// 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT +#ifndef ALPS_TUTORIAL_EXPORTED_ISING_HPP +#define ALPS_TUTORIAL_EXPORTED_ISING_HPP + +#include + +#include + +class ising_sim : public alps::mcbase { +public: + explicit ising_sim(parameters_type const & parameters, + std::size_t seed_offset = 0); + + void update() override; + void measure() override; + double fraction_completed() const override; + void save(alps::hdf5::archive & archive) const override; + void load(alps::hdf5::archive & archive) override; + +private: + std::size_t sweeps_ = 0; + std::size_t total_sweeps_ = 1; + double state_ = 1.0; +}; + +#endif diff --git a/tutorials/ngs/5_export_python/main.py b/tutorials/ngs/5_export_python/main.py new file mode 100644 index 000000000..0ca551546 --- /dev/null +++ b/tutorials/ngs/5_export_python/main.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Run the nanobind-exported C++ simulation from Python.""" + +import pyalps.ngs as ngs + +import ising_c + + +simulation = ising_c.sim(ngs.params({"SEED": 7, "SWEEPS": 10})) +simulation.run(lambda: False) +results = simulation.collectResults() +print(results) diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py new file mode 100644 index 000000000..48dbb3a7b --- /dev/null +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Exercise the public downstream simulation-export compatibility helper.""" + +import os +import tempfile + +import pyalps.hdf5 as hdf5 +import pyalps.ngs as ngs + +import ising_c + + +parameters = ngs.params({"SEED": 7, "SWEEPS": 10}) +simulation = ising_c.sim(parameters) + +assert int(simulation.parameters["SWEEPS"]) == 10 +assert len(simulation.measurements) == 1 +assert 0.0 <= simulation.random() < 1.0 +assert simulation.run(lambda: False) +assert simulation.resultNames() == ["Magnetization"] +before = simulation.collectResults() +assert before["Magnetization"].count == 10 + +with tempfile.TemporaryDirectory() as directory: + checkpoint = os.path.join(directory, "ising.h5") + with hdf5.archive(checkpoint, "w") as archive: + simulation.save(archive) + + restored = ising_c.sim(parameters) + with hdf5.archive(checkpoint, "r") as archive: + restored.load(archive) + + after = restored.collectResults() + assert restored.resultNames() == simulation.resultNames() + assert after["Magnetization"].count == before["Magnetization"].count + assert after["Magnetization"].mean == before["Magnetization"].mean + +print("downstream nanobind export: ok") From 89079f9d356bee924790c098c032903bc0e9f72b Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 01:39:57 -0500 Subject: [PATCH 23/52] test(pyalps): surface downstream exporter failures --- test/pyalps/test_binding_surface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 572f1486b..3362a61d1 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -487,11 +487,15 @@ def test_downstream_nanobind_simulation_export(tmp_path): ) completed = subprocess.run( [sys.executable, str(tutorial / "smoke_test.py")], - check=True, capture_output=True, env=environment, text=True, ) + assert completed.returncode == 0, ( + "downstream exporter smoke test failed\n" + f"stdout:\n{completed.stdout}\n" + f"stderr:\n{completed.stderr}" + ) assert "downstream nanobind export: ok" in completed.stdout From e140800d13c621007b917af16140c26df83b60ee Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 01:51:09 -0500 Subject: [PATCH 24/52] fix(pyalps): share wheel runtime with downstream modules --- CMakeLists.txt | 2 + bindings/python/pyalps/MIGRATION.md | 13 ++ cmake/ALPSConfig.cmake.in | 3 + cmake/UsePyALPS.cmake | 128 +++++++++++++++++++ tutorials/ngs/5_export_python/CMakeLists.txt | 17 +-- tutorials/ngs/5_export_python/README.md | 6 +- tutorials/ngs/5_export_python/smoke_test.py | 5 +- 7 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 cmake/UsePyALPS.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f5b3ff2b..3838df441 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,6 +351,7 @@ set(CMAKE_MACOSX_RPATH 1) ###################################################################### set(ALPS_USE_FILE ${CMAKE_INSTALL_PREFIX}/share/alps/UseALPS.cmake) +set(ALPS_PYTHON_USE_FILE ${CMAKE_INSTALL_PREFIX}/share/alps/UsePyALPS.cmake) set(Boost_INCLUDE_DIR_CONFIG ${Boost_INCLUDE_DIR}) @@ -448,6 +449,7 @@ install(DIRECTORY ${PROJECT_BINARY_DIR}/lib/xml DESTINATION ${ALPS_XML_PATH} COM add_subdirectory(cmake) install(FILES cmake/UseALPS.cmake + cmake/UsePyALPS.cmake ${PROJECT_BINARY_DIR}/cmake/ALPSConfig.cmake ${PROJECT_BINARY_DIR}/cmake/ALPSConfigVersion.cmake ${PROJECT_BINARY_DIR}/cmake/include.mk diff --git a/bindings/python/pyalps/MIGRATION.md b/bindings/python/pyalps/MIGRATION.md index 213764687..7b6495c18 100644 --- a/bindings/python/pyalps/MIGRATION.md +++ b/bindings/python/pyalps/MIGRATION.md @@ -61,6 +61,19 @@ NB_MODULE(my_sim, m) { and keep the existing export macro call. See `tutorials/ngs/5_export_python` for a complete standalone CMake build. +After creating the nanobind target, link it with the installed SDK helper: + +```cmake +include("${ALPS_PYTHON_USE_FILE}") +alps_target_link_pyalps(my_sim PYTHON_EXECUTABLE "${Python_EXECUTABLE}") +``` + +Do not link a wheel consumer directly to a second system `libalps`/HDF5 +stack. Repaired wheels carry private shared libraries, and stateful values +such as HDF5 handles are valid only in the library image that created them. +The helper selects the wheel's exact runtime when present, retains normal SDK +linking for source installs, and makes direct `import my_sim` work on macOS. + The removed `alps/python/numpy_array.hpp` API should be replaced with `nanobind::ndarray` or nanobind's STL casters. The old `alps/hdf5/python.hpp` operators accepted `boost::python::object` and have no diff --git a/cmake/ALPSConfig.cmake.in b/cmake/ALPSConfig.cmake.in index cecdccdc0..3934af217 100644 --- a/cmake/ALPSConfig.cmake.in +++ b/cmake/ALPSConfig.cmake.in @@ -33,6 +33,9 @@ SET(ALPS_VERSION "@ALPS_VERSION@") # The location of the UseALPS.cmake file. set(ALPS_USE_FILE "@ALPS_USE_FILE@") +# Helper for nanobind extensions that exchange ALPS objects with pyalps. +set(ALPS_PYTHON_USE_FILE "@ALPS_PYTHON_USE_FILE@") + # The Boost Root Dir used by ALPS set(ALPS_Boost_ROOT_DIR "@Boost_ROOT_DIR@") set(ALPS_Boost_INCLUDE_DIR "@Boost_INCLUDE_DIR_CONFIG@") diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake new file mode 100644 index 000000000..9fb1b6303 --- /dev/null +++ b/cmake/UsePyALPS.cmake @@ -0,0 +1,128 @@ +# Link a downstream nanobind module to the same ALPS runtime as pyalps. +# +# Binary wheels relocate libalps and its non-system dependencies into a +# wheel-private directory. Linking a consumer module to a separately installed +# ALPS/HDF5 stack is unsafe: objects such as hdf5::archive carry handles that +# are valid only in the HDF5 image that created them. This helper discovers a +# repaired wheel's private runtime and links the target to those exact files. +# Source/developer installs without relocated libraries keep using the normal +# ALPSConfig.cmake library paths. + +include_guard(GLOBAL) + +function(alps_target_link_pyalps target) + if(NOT TARGET "${target}") + message(FATAL_ERROR + "alps_target_link_pyalps: '${target}' is not a CMake target") + endif() + + cmake_parse_arguments(PYALPS "" "PYTHON_EXECUTABLE" "" ${ARGN}) + if(NOT PYALPS_PYTHON_EXECUTABLE) + if(Python_EXECUTABLE) + set(PYALPS_PYTHON_EXECUTABLE "${Python_EXECUTABLE}") + else() + message(FATAL_ERROR + "alps_target_link_pyalps requires PYTHON_EXECUTABLE or a preceding " + "find_package(Python ... Interpreter)") + endif() + endif() + + set(_pyalps_link_libraries ${ALPS_LIBRARIES}) + list(TRANSFORM _pyalps_link_libraries REPLACE "^hdf5-shared$" "hdf5") + + set(_pyalps_runtime_paths ${ALPS_LIBRARY_DIRS}) + if(ALPS_HDF5_INCLUDE_DIR) + get_filename_component(_pyalps_hdf5_prefix + "${ALPS_HDF5_INCLUDE_DIR}" DIRECTORY) + list(APPEND _pyalps_runtime_paths "${_pyalps_hdf5_prefix}/lib") + endif() + + # Find the package without importing it. Importing an extension while CMake + # configures would load its runtime only in this short-lived child process. + execute_process( + COMMAND "${PYALPS_PYTHON_EXECUTABLE}" -c + "import importlib.util, pathlib; s=importlib.util.find_spec('pyalps'); print(pathlib.Path(next(iter(s.submodule_search_locations))).resolve() if s and s.submodule_search_locations else '')" + OUTPUT_VARIABLE _pyalps_package_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _pyalps_location_result + ERROR_QUIET) + + if(_pyalps_location_result EQUAL 0 AND _pyalps_package_dir) + set(_pyalps_runtime_candidates + "${_pyalps_package_dir}/.dylibs" + "${_pyalps_package_dir}/../pyalps.libs") + foreach(_candidate IN LISTS _pyalps_runtime_candidates) + if(IS_DIRECTORY "${_candidate}") + get_filename_component(_pyalps_private_runtime "${_candidate}" REALPATH) + break() + endif() + endforeach() + endif() + + if(_pyalps_private_runtime) + set(_pyalps_private_libraries "") + foreach(_library IN LISTS _pyalps_link_libraries) + file(GLOB _matches LIST_DIRECTORIES FALSE + "${_pyalps_private_runtime}/lib${_library}.so*" + "${_pyalps_private_runtime}/lib${_library}-*.so*" + "${_pyalps_private_runtime}/lib${_library}.dylib" + "${_pyalps_private_runtime}/lib${_library}.*.dylib" + "${_pyalps_private_runtime}/lib${_library}-*.dylib") + list(REMOVE_DUPLICATES _matches) + list(LENGTH _matches _match_count) + if(NOT _match_count EQUAL 1) + message(FATAL_ERROR + "pyalps uses a private wheel runtime, but exactly one bundled " + "${_library} library was expected in ${_pyalps_private_runtime}; " + "found: ${_matches}") + endif() + list(GET _matches 0 _match) + list(APPEND _pyalps_private_libraries "${_match}") + + # delocate gives copied dylibs collision-resistant /DLC install names. + # Those names are intentionally not real paths, so rewrite this target's + # references to @rpath and point that rpath at the wheel directory. This + # also permits importing the consumer module before importing pyalps. + if(APPLE) + if(NOT CMAKE_OTOOL) + find_program(CMAKE_OTOOL otool REQUIRED) + endif() + if(NOT CMAKE_INSTALL_NAME_TOOL) + find_program(CMAKE_INSTALL_NAME_TOOL install_name_tool REQUIRED) + endif() + execute_process( + COMMAND "${CMAKE_OTOOL}" -D "${_match}" + OUTPUT_VARIABLE _install_names + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + string(REGEX MATCHALL "[^\r\n]+" _install_name_lines + "${_install_names}") + list(LENGTH _install_name_lines _install_name_line_count) + if(_install_name_line_count LESS 2) + message(FATAL_ERROR "Could not read the install name of ${_match}") + endif() + list(GET _install_name_lines 1 _install_name) + string(STRIP "${_install_name}" _install_name) + get_filename_component(_runtime_name "${_match}" NAME) + add_custom_command(TARGET "${target}" POST_BUILD + COMMAND "${CMAKE_INSTALL_NAME_TOOL}" -change + "${_install_name}" "@rpath/${_runtime_name}" + "$" + VERBATIM) + endif() + endforeach() + + set(_pyalps_link_libraries ${_pyalps_private_libraries}) + set(_pyalps_runtime_paths "${_pyalps_private_runtime}") + message(STATUS + "${target}: using pyalps wheel runtime at ${_pyalps_private_runtime}") + else() + target_link_directories("${target}" PRIVATE ${_pyalps_runtime_paths}) + endif() + + target_link_libraries("${target}" PRIVATE ${_pyalps_link_libraries}) + set_property(TARGET "${target}" APPEND PROPERTY + BUILD_RPATH ${_pyalps_runtime_paths}) + set_property(TARGET "${target}" APPEND PROPERTY + INSTALL_RPATH ${_pyalps_runtime_paths}) +endfunction() diff --git a/tutorials/ngs/5_export_python/CMakeLists.txt b/tutorials/ngs/5_export_python/CMakeLists.txt index 8d6f954fd..d1ba4afb9 100644 --- a/tutorials/ngs/5_export_python/CMakeLists.txt +++ b/tutorials/ngs/5_export_python/CMakeLists.txt @@ -16,27 +16,16 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(_alps_libraries ${ALPS_LIBRARIES}) -list(TRANSFORM _alps_libraries REPLACE "^hdf5-shared$" "hdf5") separate_arguments(_alps_link_options NATIVE_COMMAND "${ALPS_EXTRA_LINKER_FLAGS}") separate_arguments(_alps_compile_options NATIVE_COMMAND "${ALPS_CMAKE_CXX_FLAGS}") -link_directories(${ALPS_LIBRARY_DIRS}) -set(_alps_runtime_paths ${ALPS_LIBRARY_DIRS}) -if(ALPS_HDF5_INCLUDE_DIR) - get_filename_component(_hdf5_prefix "${ALPS_HDF5_INCLUDE_DIR}" DIRECTORY) - link_directories("${_hdf5_prefix}/lib") - list(APPEND _alps_runtime_paths "${_hdf5_prefix}/lib") -endif() - nanobind_add_module(ising_c NB_STATIC export2py.cpp ising.cpp) target_include_directories(ising_c PRIVATE ${ALPS_INCLUDE_DIRS} ${ALPS_EXTRA_INCLUDE_DIRS}) target_compile_options(ising_c PRIVATE ${_alps_compile_options}) target_compile_definitions(ising_c PRIVATE ${ALPS_EXTRA_DEFINITIONS}) -target_link_libraries(ising_c PRIVATE ${_alps_libraries}) target_link_options(ising_c PRIVATE ${_alps_link_options}) -set_target_properties(ising_c PROPERTIES - INSTALL_RPATH "${_alps_runtime_paths}" - BUILD_RPATH "${_alps_runtime_paths}") +include("${ALPS_PYTHON_USE_FILE}") +alps_target_link_pyalps(ising_c + PYTHON_EXECUTABLE "${Python_EXECUTABLE}") diff --git a/tutorials/ngs/5_export_python/README.md b/tutorials/ngs/5_export_python/README.md index 258aa1e68..67fed0ce4 100644 --- a/tutorials/ngs/5_export_python/README.md +++ b/tutorials/ngs/5_export_python/README.md @@ -17,4 +17,8 @@ For an old export source, replace `BOOST_PYTHON_MODULE(name) {` with `ALPS_EXPORT_SIM_TO_PYTHON(PythonName, SimulationClass)` call remains valid. The helper imports pyalps' owning extension modules before registering the derived class, so ALPS parameter, archive, observable, and result types are -shared safely through nanobind's process-wide type registry. +shared safely through nanobind's process-wide type registry. The example also +uses `alps_target_link_pyalps`, supplied by `ALPS_PYTHON_USE_FILE`, to link a +consumer to the exact `libalps`, Boost, and HDF5 copies carried by a repaired +pyalps wheel. This is required for stateful library objects such as HDF5 +handles; do not replace it with a second system HDF5 linkage. diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py index 48dbb3a7b..a33b7777a 100644 --- a/tutorials/ngs/5_export_python/smoke_test.py +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -4,11 +4,12 @@ import os import tempfile +# Importing the consumer first verifies its wheel-runtime rpath. Its module +# initializer loads the owning pyalps bindings before registering C++ types. +import ising_c import pyalps.hdf5 as hdf5 import pyalps.ngs as ngs -import ising_c - parameters = ngs.params({"SEED": 7, "SWEEPS": 10}) simulation = ising_c.sim(parameters) From 3da11b826eeb19e540435b91119e4b83ac584d1b Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 02:02:07 -0500 Subject: [PATCH 25/52] chore(cmake): add license header to PyALPS helper --- cmake/UsePyALPS.cmake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake index 9fb1b6303..507c7e386 100644 --- a/cmake/UsePyALPS.cmake +++ b/cmake/UsePyALPS.cmake @@ -1,3 +1,6 @@ +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT +# # Link a downstream nanobind module to the same ALPS runtime as pyalps. # # Binary wheels relocate libalps and its non-system dependencies into a From f97a496da7899fdf4de8c7dd4bd971e39e896b47 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 02:29:31 -0500 Subject: [PATCH 26/52] docs(pyalps): remove standalone migration guide --- bindings/python/pyalps/MIGRATION.md | 81 ----------------------------- bindings/python/pyalps/README.md | 2 - 2 files changed, 83 deletions(-) delete mode 100644 bindings/python/pyalps/MIGRATION.md diff --git a/bindings/python/pyalps/MIGRATION.md b/bindings/python/pyalps/MIGRATION.md deleted file mode 100644 index 7b6495c18..000000000 --- a/bindings/python/pyalps/MIGRATION.md +++ /dev/null @@ -1,81 +0,0 @@ -# Migrating from the Boost.Python pyalps build - -The public Python API is preserved wherever it maps to native ALPS values. -The nanobind build intentionally does not keep arbitrary Python objects inside -`alps::params` or the C++ library. - -## Parameters - -`pyalps.ngs.params` accepts native booleans, 32-bit integers, floating-point -and complex numbers, strings, homogeneous Python sequences, NumPy scalar -arrays, and one-dimensional NumPy arrays. Values are copied into native C++ -storage. Multidimensional arrays, `None`, dictionaries, arbitrary objects, and -integers outside ALPS' 32-bit parameter range raise `TypeError` rather than -being retained as opaque Python objects. Sequence values are returned as -lists, irrespective of whether the input was a list, tuple, or NumPy array. - -## MPI - -Install `pyalps[mpi]` to use `pyalps.mpi`. The module provides the commonly -used Boost.MPI Python surface (`world`, `rank`, `size`, `Communicator`, -point-to-point methods, collectives, status/request names, and `Timer`) on top -of mpi4py. Ordinary pyalps wheels remain independent of any MPI runtime. - -The historical `mcbase(..., communicator)` argument is still accepted. It is -ignored, as it was by the Boost.Python wrapper; `alps::mcbase` itself has no -communicator constructor. Use `pyalps.mpi` for Python communication and ALPS' -C++ MPI adapters for MPI-aware C++ simulations. - -Boost.MPI's Python-object serialization bridge and skeleton/content API are -not reproduced. Hybrid applications should use mpi4py's typed buffer API or -an application-specific native C++ protocol. - -## Compiled module paths and DWA vectors - -Legacy paths such as `pyalps.pyalea_c` and `pyalps.dwa_c` remain aliases of -the extensions now stored under `pyalps._ext`. The preferred stable import is -still `pyalps.cxx.pyalea_c` for core extensions and `pyalps.dwa` for DWA. - -DWA's former `std_vector_*` constructors are compatibility aliases for -Python's `list`. DWA methods return list snapshots, which avoids exposing -mutable C++ container proxies and accepts ordinary Python sequences directly. - -## Exporting downstream C++ simulations - -The public header `` now implements -the export helper with nanobind while keeping the -`ALPS_EXPORT_SIM_TO_PYTHON` macro. Change the module declaration in an old -export source from: - -```cpp -BOOST_PYTHON_MODULE(my_sim) { -``` - -to: - -```cpp -#include -NB_MODULE(my_sim, m) { -``` - -and keep the existing export macro call. See -`tutorials/ngs/5_export_python` for a complete standalone CMake build. - -After creating the nanobind target, link it with the installed SDK helper: - -```cmake -include("${ALPS_PYTHON_USE_FILE}") -alps_target_link_pyalps(my_sim PYTHON_EXECUTABLE "${Python_EXECUTABLE}") -``` - -Do not link a wheel consumer directly to a second system `libalps`/HDF5 -stack. Repaired wheels carry private shared libraries, and stateful values -such as HDF5 handles are valid only in the library image that created them. -The helper selects the wheel's exact runtime when present, retains normal SDK -linking for source installs, and makes direct `import my_sim` work on macOS. - -The removed `alps/python/numpy_array.hpp` API should be replaced with -`nanobind::ndarray` or nanobind's STL casters. The old -`alps/hdf5/python.hpp` operators accepted `boost::python::object` and have no -Python-object-free equivalent; use typed `alps::hdf5::archive` operations in -C++ or `pyalps.hdf5` at the Python boundary. diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 126aeb3fc..e25ab8a98 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -9,8 +9,6 @@ python -m pip install pyalps Install `pyalps[plot]` to use the Matplotlib plotting helpers. Install `pyalps[mpi]` for the mpi4py-backed `pyalps.mpi` compatibility layer. -Projects moving from the Boost.Python build should also read -[the nanobind migration guide](https://github.com/ALPSim/ALPS/blob/master/bindings/python/pyalps/MIGRATION.md). The bindings are built as a standalone `scikit-build-core` project using nanobind. A source build requires Python 3.10 or newer, CMake 3.21 or newer, From 2b426086404816953eeaea377f3a48394a3e82b9 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 02:36:39 -0500 Subject: [PATCH 27/52] fix(pyalps): resolve relocated Linux libraries --- cmake/UsePyALPS.cmake | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake index 507c7e386..ad314b5f8 100644 --- a/cmake/UsePyALPS.cmake +++ b/cmake/UsePyALPS.cmake @@ -65,19 +65,39 @@ function(alps_target_link_pyalps target) if(_pyalps_private_runtime) set(_pyalps_private_libraries "") foreach(_library IN LISTS _pyalps_link_libraries) + # Installed ALPSConfig files may record dependencies as bare linker + # names (alps), linker flags (-lalps), or absolute paths + # (/usr/lib64/liblapack.so). Wheel repair tools rename all three forms + # to a private file such as liblapack-.so. Normalize the original + # entry to its library stem before looking up that repaired file. + set(_library_stem "${_library}") + if(IS_ABSOLUTE "${_library_stem}") + get_filename_component(_library_stem "${_library_stem}" NAME) + endif() + string(REGEX REPLACE "^-l" "" _library_stem "${_library_stem}") + string(REGEX REPLACE "^lib" "" _library_stem "${_library_stem}") + string(REGEX REPLACE "\\.so(\\.[0-9]+)*$" "" _library_stem + "${_library_stem}") + string(REGEX REPLACE "(\\.[0-9]+)*\\.dylib$" "" _library_stem + "${_library_stem}") + string(REGEX REPLACE "\\.a$" "" _library_stem "${_library_stem}") + if(_library_stem STREQUAL "hdf5-shared") + set(_library_stem "hdf5") + endif() + file(GLOB _matches LIST_DIRECTORIES FALSE - "${_pyalps_private_runtime}/lib${_library}.so*" - "${_pyalps_private_runtime}/lib${_library}-*.so*" - "${_pyalps_private_runtime}/lib${_library}.dylib" - "${_pyalps_private_runtime}/lib${_library}.*.dylib" - "${_pyalps_private_runtime}/lib${_library}-*.dylib") + "${_pyalps_private_runtime}/lib${_library_stem}.so*" + "${_pyalps_private_runtime}/lib${_library_stem}-*.so*" + "${_pyalps_private_runtime}/lib${_library_stem}.dylib" + "${_pyalps_private_runtime}/lib${_library_stem}.*.dylib" + "${_pyalps_private_runtime}/lib${_library_stem}-*.dylib") list(REMOVE_DUPLICATES _matches) list(LENGTH _matches _match_count) if(NOT _match_count EQUAL 1) message(FATAL_ERROR "pyalps uses a private wheel runtime, but exactly one bundled " - "${_library} library was expected in ${_pyalps_private_runtime}; " - "found: ${_matches}") + "${_library_stem} library (from '${_library}') was expected in " + "${_pyalps_private_runtime}; found: ${_matches}") endif() list(GET _matches 0 _match) list(APPEND _pyalps_private_libraries "${_match}") @@ -114,6 +134,7 @@ function(alps_target_link_pyalps target) VERBATIM) endif() endforeach() + list(REMOVE_DUPLICATES _pyalps_private_libraries) set(_pyalps_link_libraries ${_pyalps_private_libraries}) set(_pyalps_runtime_paths "${_pyalps_private_runtime}") From cc92ee29653e33914f996505957691173bff931d Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 02:59:17 -0500 Subject: [PATCH 28/52] fix(pyalps): resolve transitive wheel libraries --- cmake/UsePyALPS.cmake | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake index ad314b5f8..abf767c65 100644 --- a/cmake/UsePyALPS.cmake +++ b/cmake/UsePyALPS.cmake @@ -138,6 +138,13 @@ function(alps_target_link_pyalps target) set(_pyalps_link_libraries ${_pyalps_private_libraries}) set(_pyalps_runtime_paths "${_pyalps_private_runtime}") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # GNU DT_RUNPATH is searched only for direct dependencies. Wheel + # libraries such as LAPACK can themselves depend on relocated runtime + # libraries (for example auditwheel's libgfortran copy), so emit the + # transitive DT_RPATH tag for this consumer module instead. + target_link_options("${target}" PRIVATE "LINKER:--disable-new-dtags") + endif() message(STATUS "${target}: using pyalps wheel runtime at ${_pyalps_private_runtime}") else() From 439f85c8e98a53de5179377816d273d28d3b7f8c Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 10:47:56 -0500 Subject: [PATCH 29/52] fix(pyalps): preserve downstream mcbase identity --- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 45 +++++++++++--------- src/alps/mcbase.cpp | 2 + src/alps/mcbase.hpp | 1 + src/alps/ngs/detail/export_sim_to_python.hpp | 5 +-- tutorials/ngs/5_export_python/smoke_test.py | 2 + 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index 32e7d1892..9fe1d13f5 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -31,8 +31,9 @@ // // Trampoline (PyMCBase) forwards the three pure-virtual mcbase methods // (update / measure / fraction_completed) back into the Python subclass -// through nanobind's trampoline support. The old wrapper -// pattern becomes a standard trampoline-plus-alias pair. +// through nanobind's trampoline support. Binding mcbase itself with +// PyMCBase as its alias preserves the public base class of downstream +// simulations, as in the Boost.Python bindings. // // Params ingestion: the public alps::mcbase ctor wants an alps::params. // We convert nb::dict → alps::params at the binding boundary through @@ -48,9 +49,13 @@ namespace nb = nanobind; #include #include -#include +#include +#include #include "../dict_to_params.hpp" namespace alps { + static_assert(std::has_virtual_destructor::value, + "mcbase must safely destroy nanobind trampoline aliases"); + // Trampoline: holds Python overrides for pure-virtuals. The // protected mcbase members (random / parameters / measurements) // are accessed via lambdas in the binding below, which friend-in @@ -91,19 +96,10 @@ namespace alps { alps::random01 & get_random() { return random; } mcbase::parameters_type & get_parameters() { return parameters; } alps::mcobservables & get_measurements() { return measurements; } - // mcbase::run takes a std::function; wrap a Python - // callable so the stop_callback can be driven from Python. - bool run_py(nb::object stop_callback) { - return mcbase::run([stop_callback]() -> bool { - nb::gil_scoped_acquire gil; - return nb::cast(stop_callback()); - }); - } }; } NB_MODULE(pyngsbase_c, m) { - nb::class_(m, "_mcbase", nb::never_destruct()); - nb::class_(m, "mcbase") + nb::class_(m, "mcbase") // Retain the legacy third argument without binding Boost.MPI. The // Boost.Python-era constructor accepted a communicator but never // passed it to alps::mcbase (which has no communicator constructor), @@ -115,20 +111,31 @@ NB_MODULE(pyngsbase_c, m) { nb::arg("communicator") = nb::none()) .def_prop_ro( "random", - [](alps::PyMCBase & self) -> alps::random01 & { return self.get_random(); }, + [](alps::mcbase & self) -> alps::random01 & { + return dynamic_cast(self).get_random(); + }, nb::rv_policy::reference_internal) .def_prop_ro( "parameters", - [](alps::PyMCBase & self) -> alps::mcbase::parameters_type & { return self.get_parameters(); }, + [](alps::mcbase & self) -> alps::mcbase::parameters_type & { + return dynamic_cast(self).get_parameters(); + }, nb::rv_policy::reference_internal) .def_prop_ro( "measurements", - [](alps::PyMCBase & self) -> alps::mcobservables & { return self.get_measurements(); }, + [](alps::mcbase & self) -> alps::mcobservables & { + return dynamic_cast(self).get_measurements(); + }, nb::rv_policy::reference_internal) .def("run", - [](alps::PyMCBase & self, nb::object cb) { return self.run_py(std::move(cb)); }) - // Pure-virtual methods: bound on the base class; the trampoline's - // The trampoline forwards the call into the Python subclass. + [](alps::mcbase & self, nb::object stop_callback) { + return self.run([stop_callback = std::move(stop_callback)]() -> bool { + nb::gil_scoped_acquire gil; + return nb::cast(stop_callback()); + }); + }) + // Pure-virtual methods are bound on the base class; the trampoline + // forwards each call into the Python subclass. .def("update", &alps::mcbase::update) .def("measure", &alps::mcbase::measure) .def("fraction_completed", &alps::mcbase::fraction_completed) diff --git a/src/alps/mcbase.cpp b/src/alps/mcbase.cpp index 8ebe974d7..0bbecbb4e 100644 --- a/src/alps/mcbase.cpp +++ b/src/alps/mcbase.cpp @@ -24,6 +24,8 @@ namespace alps { alps::ngs::signal::listen(); } + mcbase::~mcbase() = default; + void mcbase::save(boost::filesystem::path const & filename) const { alps::hdf5::archive ar(filename, "w"); ar["/simulation/realizations/0/clones/0"] << *this; diff --git a/src/alps/mcbase.hpp b/src/alps/mcbase.hpp index 81c001533..cffaef6cb 100644 --- a/src/alps/mcbase.hpp +++ b/src/alps/mcbase.hpp @@ -46,6 +46,7 @@ namespace alps { #endif mcbase(parameters_type const & parms, std::size_t seed_offset = 0); + virtual ~mcbase(); virtual void update() = 0; virtual void measure() = 0; diff --git a/src/alps/ngs/detail/export_sim_to_python.hpp b/src/alps/ngs/detail/export_sim_to_python.hpp index b5dcb2b92..758044c54 100644 --- a/src/alps/ngs/detail/export_sim_to_python.hpp +++ b/src/alps/ngs/detail/export_sim_to_python.hpp @@ -35,10 +35,7 @@ class exported_simulation : public Simulation { std::size_t seed_offset = 0) : Simulation(parameters, seed_offset) {} - // mcbase predates virtual-destructor guidance. The Python-owned concrete - // wrapper is nevertheless polymorphic, so give this boundary type its own - // virtual destructor and ensure nanobind always destroys the full object. - virtual ~exported_simulation() = default; + ~exported_simulation() override = default; bool run_python(nb::object stop_callback) { return Simulation::run([stop_callback]() -> bool { diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py index a33b7777a..ba5b5be08 100644 --- a/tutorials/ngs/5_export_python/smoke_test.py +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -14,6 +14,8 @@ parameters = ngs.params({"SEED": 7, "SWEEPS": 10}) simulation = ising_c.sim(parameters) +assert issubclass(ising_c.sim, ngs.mcbase) +assert isinstance(simulation, ngs.mcbase) assert int(simulation.parameters["SWEEPS"]) == 10 assert len(simulation.measurements) == 1 assert 0.0 <= simulation.random() < 1.0 From eb0e14c476c2a5d360cbcbf090c3a9e1adf127d8 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 17:14:40 -0500 Subject: [PATCH 30/52] fix(pyalps): close remaining audit gaps --- bindings/python/pyalps/cpp/dict_to_params.hpp | 9 ++++++--- .../pyalps/cpp/ngs/extract_from_pyobject.hpp | 3 ++- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 17 ++++------------- bindings/python/pyalps/cpp/ngs/params.cpp | 14 ++------------ src/alps/mcbase.hpp | 11 ++++++++--- src/alps/ngs/lib/params.cpp | 7 ++++++- src/alps/ngs/params.hpp | 5 +++++ test/ngs/params/assign.cpp | 3 +++ tutorials/ngs/5_export_python/smoke_test.py | 6 ++++++ 9 files changed, 42 insertions(+), 33 deletions(-) diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index 9b7bb8cb4..db86a7d28 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -8,6 +8,7 @@ // module ingests parameters identically. #ifndef PYALPS_DICT_TO_PARAMS_HPP #define PYALPS_DICT_TO_PARAMS_HPP +#include "numpy_compat.hpp" #include #include #include @@ -40,12 +41,14 @@ inline bool is_bool_like(PyObject * raw) { inline bool is_numpy_array(nb::handle value) { // isinstance, rather than an exact tp_name comparison, keeps ndarray // subclasses (for example an unmasked numpy.ma.MaskedArray) on the same - // native-copy path. Import lookup itself is cached by Python. - return nb::isinstance(value, nb::module_::import_("numpy").attr("ndarray")); + // native-copy path. Reuse the process-lifetime module handle shared by + // the other NumPy conversion helpers. + return nb::isinstance( + value, alps::python::numpy_module().attr("ndarray")); } inline char numpy_scalar_kind(nb::handle value) { - nb::object numpy = nb::module_::import_("numpy"); + nb::handle numpy = alps::python::numpy_module(); if (!nb::isinstance(value, numpy.attr("generic"))) return '\0'; std::string const kind = nb::cast( diff --git a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp index 735ca0f5a..6619cbd85 100644 --- a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp +++ b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp @@ -9,6 +9,7 @@ /// . #ifndef PYALPS_NGS_EXTRACT_FROM_PYOBJECT_HPP #define PYALPS_NGS_EXTRACT_FROM_PYOBJECT_HPP + #include "../numpy_compat.hpp" #include #include #include @@ -87,7 +88,7 @@ else if (dtype == "numpy.ndarray" || nb_::isinstance( data, - nb_::module_::import_("numpy").attr("ndarray"))) { + alps::python::numpy_module().attr("ndarray"))) { // Reject non-native byte order explicitly. nanobind's // failed ndarray cast would otherwise surface only as // the unhelpful message "std::bad_cast". diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index 9fe1d13f5..bde258c0e 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -56,11 +56,7 @@ namespace alps { static_assert(std::has_virtual_destructor::value, "mcbase must safely destroy nanobind trampoline aliases"); - // Trampoline: holds Python overrides for pure-virtuals. The - // protected mcbase members (random / parameters / measurements) - // are accessed via lambdas in the binding below, which friend-in - // through PyMCBase (a protected member is visible to a derived - // class's own member functions / friends). + // Trampoline: holds Python overrides for pure-virtuals. class PyMCBase : public mcbase { public: // Slot count = the number of NB_OVERRIDE* calls below. @@ -91,11 +87,6 @@ namespace alps { void load(alps::hdf5::archive & ar) override { NB_OVERRIDE(load, ar); } - // Accessors for protected mcbase members. Called from the - // binding lambdas below (they friend-in through PyMCBase). - alps::random01 & get_random() { return random; } - mcbase::parameters_type & get_parameters() { return parameters; } - alps::mcobservables & get_measurements() { return measurements; } }; } NB_MODULE(pyngsbase_c, m) { @@ -112,19 +103,19 @@ NB_MODULE(pyngsbase_c, m) { .def_prop_ro( "random", [](alps::mcbase & self) -> alps::random01 & { - return dynamic_cast(self).get_random(); + return self.get_random(); }, nb::rv_policy::reference_internal) .def_prop_ro( "parameters", [](alps::mcbase & self) -> alps::mcbase::parameters_type & { - return dynamic_cast(self).get_parameters(); + return self.get_parameters(); }, nb::rv_policy::reference_internal) .def_prop_ro( "measurements", [](alps::mcbase & self) -> alps::mcobservables & { - return dynamic_cast(self).get_measurements(); + return self.get_measurements(); }, nb::rv_policy::reference_internal) .def("run", diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index e4e5781f0..ff575a6b2 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -44,18 +44,8 @@ void params_setitem(alps::params & self, nb::object const & key_obj, nb::handle } nb::object params_getitem(alps::params & self, nb::object const & key_obj) { std::string key = nb::cast(nb::str(key_obj)); - // defined() answers the (common) miss with one map lookup; - // paramiterator steps re-do a map find each, so walking the whole - // container to conclude "absent" would be much slower. - if (!self.defined(key)) - return nb::none(); - // params doesn't expose the underlying map directly, but - // paramiterator yields (key, paramvalue) pairs; walk it to find the - // entry and hand the variant to paramvalue_to_py. - for (auto it = self.begin(); it != self.end(); ++it) - if (it->first == key) - return paramvalue_to_py(it->second); - return nb::none(); // defensive — defined()==true should guarantee a hit + alps::detail::paramvalue const * value = self.find(key); + return value ? paramvalue_to_py(*value) : nb::none(); } void params_delitem(alps::params & self, nb::object const & key_obj) { self.erase(nb::cast(nb::str(key_obj))); diff --git a/src/alps/mcbase.hpp b/src/alps/mcbase.hpp index cffaef6cb..225e7f909 100644 --- a/src/alps/mcbase.hpp +++ b/src/alps/mcbase.hpp @@ -26,7 +26,7 @@ namespace alps { class ALPS_DECL mcbase { - protected: + public: #ifdef ALPS_NGS_USE_NEW_ALEA typedef alps::accumulator::accumulator_set observable_collection_type; @@ -34,8 +34,6 @@ namespace alps { typedef alps::mcobservables observable_collection_type; #endif - public: - typedef alps::params parameters_type; typedef std::vector result_names_type; @@ -63,6 +61,13 @@ namespace alps { virtual void save(alps::hdf5::archive & ar) const; virtual void load(alps::hdf5::archive & ar); + // Non-virtual accessors for language bindings and downstream + // exporters. Keeping these on the actual base class avoids + // assuming that every derived simulation is a Python trampoline. + alps::random01 & get_random() { return random; } + parameters_type & get_parameters() { return parameters; } + observable_collection_type & get_measurements() { return measurements; } + protected: parameters_type parameters; diff --git a/src/alps/ngs/lib/params.cpp b/src/alps/ngs/lib/params.cpp index 37459c7e5..3ced4fe23 100644 --- a/src/alps/ngs/lib/params.cpp +++ b/src/alps/ngs/lib/params.cpp @@ -45,7 +45,7 @@ namespace alps { void params::erase(std::string const & key) { if (!defined(key)) throw std::invalid_argument("the key " + key + " does not exists" + ALPS_STACKTRACE); - keys.erase(find(keys.begin(), keys.end(), key)); + keys.erase(std::find(keys.begin(), keys.end(), key)); values.erase(key); } @@ -69,6 +69,11 @@ namespace alps { return values.find(key) != values.end(); } + detail::paramvalue const * params::find(std::string const & key) const { + std::map::const_iterator it = values.find(key); + return it == values.end() ? nullptr : &it->second; + } + params::iterator params::begin() { return iterator(*this, keys.begin()); } diff --git a/src/alps/ngs/params.hpp b/src/alps/ngs/params.hpp index b185790db..f43d47b05 100644 --- a/src/alps/ngs/params.hpp +++ b/src/alps/ngs/params.hpp @@ -69,6 +69,11 @@ namespace alps { bool defined(std::string const &) const; + // Direct native lookup for consumers that need to inspect the + // stored variant. The returned pointer remains owned by params + // and is null when the key is absent. + detail::paramvalue const * find(std::string const &) const; + iterator begin(); const_iterator begin() const; diff --git a/test/ngs/params/assign.cpp b/test/ngs/params/assign.cpp index 2c0336842..237312775 100644 --- a/test/ngs/params/assign.cpp +++ b/test/ngs/params/assign.cpp @@ -40,6 +40,9 @@ int main() { parms["std::string"] = std::string("asdf"); assert(parms["std::vector"].cast >() == bool_vector); + assert(parms.find("int") != nullptr); + assert(parms.find("int")->cast() == 1); + assert(parms.find("missing") == nullptr); std::cout << parms << std::endl; return 0; diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py index ba5b5be08..bc4a7e381 100644 --- a/tutorials/ngs/5_export_python/smoke_test.py +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -19,6 +19,12 @@ assert int(simulation.parameters["SWEEPS"]) == 10 assert len(simulation.measurements) == 1 assert 0.0 <= simulation.random() < 1.0 +# The base descriptors must work for a downstream C++ simulation too. This +# used to throw std::bad_cast because mcbase assumed every instance was its +# Python trampoline alias. +assert ngs.mcbase.parameters.__get__(simulation) is simulation.parameters +assert ngs.mcbase.measurements.__get__(simulation) is simulation.measurements +assert ngs.mcbase.random.__get__(simulation) is simulation.random assert simulation.run(lambda: False) assert simulation.resultNames() == ["Magnetization"] before = simulation.collectResults() From eb917802ebcf8b02c67781fc34509fbb246a9638 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 19 Aug 2026 17:45:43 -0500 Subject: [PATCH 31/52] fix(pyalps): restore Python signal handlers after CT-QMC --- .../dmft/qmc/hybridization/hybmain.cpp | 3 +- .../dmft/qmc/interaction_expansion2/main.cpp | 2 + .../interaction_expansion2/observables.cpp | 12 +-- .../pyalps/cpp/scoped_signal_handlers.hpp | 73 +++++++++++++++++++ test/pyalps/test_binding_surface.py | 67 +++++++++++++++++ 5 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 bindings/python/pyalps/cpp/scoped_signal_handlers.hpp diff --git a/applications/dmft/qmc/hybridization/hybmain.cpp b/applications/dmft/qmc/hybridization/hybmain.cpp index ac063fda9..45b307faa 100644 --- a/applications/dmft/qmc/hybridization/hybmain.cpp +++ b/applications/dmft/qmc/hybridization/hybmain.cpp @@ -34,9 +34,11 @@ int global_mpi_rank; #ifdef BUILD_PYTHON_MODULE #include "dict_to_params.hpp" +#include "scoped_signal_handlers.hpp" namespace nb = nanobind; void solve(nb::dict const & parms_){ + pyalps::scoped_signal_handlers signal_handlers; alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else @@ -140,4 +142,3 @@ NB_MODULE(cthyb, m) { #endif - diff --git a/applications/dmft/qmc/interaction_expansion2/main.cpp b/applications/dmft/qmc/interaction_expansion2/main.cpp index 74062ab7c..cf1d3f9b0 100644 --- a/applications/dmft/qmc/interaction_expansion2/main.cpp +++ b/applications/dmft/qmc/interaction_expansion2/main.cpp @@ -31,9 +31,11 @@ int global_mpi_rank; #ifdef BUILD_PYTHON_MODULE #include "dict_to_params.hpp" +#include "scoped_signal_handlers.hpp" namespace nb = nanobind; void solve(nb::dict const & parms_){ + pyalps::scoped_signal_handlers signal_handlers; alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string output_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); #else diff --git a/applications/dmft/qmc/interaction_expansion2/observables.cpp b/applications/dmft/qmc/interaction_expansion2/observables.cpp index 774c239f1..ebe47dcdb 100644 --- a/applications/dmft/qmc/interaction_expansion2/observables.cpp +++ b/applications/dmft/qmc/interaction_expansion2/observables.cpp @@ -101,13 +101,13 @@ void InteractionExpansion::initialize_observables(void) sz_name<<"Sz_"< + +#if !defined(BOOST_MSVC) && !defined(ALPS_NGS_NO_SIGNALS) +#include +#include +#include +#endif + +namespace pyalps { + +// ALPS applications own process signal handlers while they run. Python is an +// embedded runtime, however, so its handlers must be put back before control +// returns to the interpreter. Reinstalling the ALPS handlers here also makes +// repeated solve() calls work after the preceding guard restored Python's. +class scoped_signal_handlers { +public: + scoped_signal_handlers() { +#if !defined(BOOST_MSVC) && !defined(ALPS_NGS_NO_SIGNALS) + for (std::size_t i = 0; i < signal_numbers_.size(); ++i) + saved_[i].valid = sigaction(signal_numbers_[i], NULL, &saved_[i].action) == 0; + + struct sigaction action; + std::memset(&action, 0, sizeof(action)); + action.sa_handler = &alps::ngs::signal::slot; + for (std::size_t i = 0; i < termination_signal_count_; ++i) + sigaction(signal_numbers_[i], &action, NULL); + + action.sa_handler = &alps::ngs::signal::segfault; + for (std::size_t i = termination_signal_count_; i < signal_numbers_.size(); ++i) + sigaction(signal_numbers_[i], &action, NULL); +#endif + } + + ~scoped_signal_handlers() { +#if !defined(BOOST_MSVC) && !defined(ALPS_NGS_NO_SIGNALS) + for (std::size_t i = 0; i < signal_numbers_.size(); ++i) + if (saved_[i].valid) + sigaction(signal_numbers_[i], &saved_[i].action, NULL); +#endif + } + + scoped_signal_handlers(scoped_signal_handlers const &) = delete; + scoped_signal_handlers & operator=(scoped_signal_handlers const &) = delete; + +private: +#if !defined(BOOST_MSVC) && !defined(ALPS_NGS_NO_SIGNALS) + struct saved_action { + struct sigaction action; + bool valid = false; + }; + + static constexpr std::array signal_numbers_ = {{ + SIGINT, SIGTERM, SIGXCPU, SIGQUIT, SIGUSR1, SIGUSR2, SIGSEGV, SIGBUS + }}; + static constexpr std::size_t termination_signal_count_ = 6; + std::array saved_; +#endif +}; + +#if !defined(BOOST_MSVC) && !defined(ALPS_NGS_NO_SIGNALS) +constexpr std::array scoped_signal_handlers::signal_numbers_; +constexpr std::size_t scoped_signal_handlers::termination_signal_count_; +#endif + +} // namespace pyalps + +#endif diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 3362a61d1..021c69342 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -10,6 +10,7 @@ import importlib import os from pathlib import Path +import signal import subprocess import sys import tempfile @@ -242,6 +243,72 @@ def test_optional_application_extension_surface(): assert len(bands.t()) == 3 +def test_ctqmc_solvers_restore_python_signal_handlers(tmp_path, monkeypatch): + from pyalps import cthyb, ctint + import pyalps.hdf5 as hdf5 + + monkeypatch.chdir(tmp_path) + + delta_path = tmp_path / "delta.dat" + delta_path.write_text("".join(f"{i} -0.5 -0.5\n" for i in range(11))) + cthyb_params = { + "SWEEPS": 1, + "MAX_TIME": 1, + "THERMALIZATION": 0, + "SEED": 0, + "N_MEAS": 1, + "N_HISTOGRAM_ORDERS": 4, + "N_ORBITALS": 2, + "U": 1.0, + "MU": 0.5, + "DELTA": str(delta_path), + "N_TAU": 10, + "BETA": 1.0, + "TEXT_OUTPUT": 0, + "BASENAME": str(tmp_path / "cthyb-signal"), + } + + ctint_input = tmp_path / "ctint-input.h5" + archive = hdf5.archive(str(ctint_input), "w") + bare_green = np.asarray([-1j, -0.3j, -0.2j, -0.1j]) + archive["/G0_0"] = bare_green + archive["/G0_1"] = bare_green + del archive + ctint_params = { + "SWEEPS": 1, + "MAX_TIME": 1, + "THERMALIZATION": 0, + "BETA": 1.0, + "U": 1.0, + "MU": 0.5, + "ALPHA": 0.5, + "N_MATSUBARA": 4, + "N_TAU": 4, + "INFILE": str(ctint_input), + "BASENAME": str(tmp_path / "ctint-signal"), + } + + calls = [] + + def python_sigint_handler(signum, frame): + calls.append((signum, frame)) + + previous_handler = signal.signal(signal.SIGINT, python_sigint_handler) + try: + # Run each solver twice: restoration alone is not enough if ALPS' own + # handlers are not reinstalled for the next embedded call. + for solver, params in ((cthyb, cthyb_params), (ctint, ctint_params)): + for _ in range(2): + solver.solve(params) + assert signal.getsignal(signal.SIGINT) is python_sigint_handler + signal.raise_signal(signal.SIGINT) + assert calls[-1][0] == signal.SIGINT + finally: + signal.signal(signal.SIGINT, previous_handler) + + assert len(calls) == 4 + + def test_mpi4py_compatibility_surface(): pytest.importorskip("mpi4py") import operator From e9930d0e286381de735d09220dce226a3ac4e6f0 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 20 Aug 2026 22:44:33 -0500 Subject: [PATCH 32/52] fix(pyalps): complete packaging and compatibility follow-ups Finish the Python 3 and archive compatibility repairs, bundle the ALPS runtime and applications once, retain ordinary per-version wheels for downstream nanobind interoperability, and follow master's DWA removal. --- .github/workflows/build_wheels.yml | 20 +- CMakePresets.json | 13 +- bindings/python/pyalps/CMakeLists.txt | 164 ++++++++-- bindings/python/pyalps/README.md | 23 +- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 44 +++ bindings/python/pyalps/cpp/ngs/mcbase.cpp | 17 +- bindings/python/pyalps/cpp/ngs/observable.cpp | 30 +- .../python/pyalps/cpp/ngs/observables.cpp | 18 +- bindings/python/pyalps/cpp/ngs/results.cpp | 27 +- bindings/python/pyalps/pyproject.toml | 1 - bindings/python/pyalps/src/pyalps/__init__.py | 11 +- bindings/python/pyalps/src/pyalps/alea.py | 20 +- bindings/python/pyalps/src/pyalps/apptest.py | 1 - bindings/python/pyalps/src/pyalps/cxx.py | 1 - .../pyalps/src/pyalps/floatwitherror.py | 13 +- bindings/python/pyalps/src/pyalps/hlist.py | 18 +- bindings/python/pyalps/src/pyalps/lattice.py | 2 +- bindings/python/pyalps/src/pyalps/load.py | 22 +- bindings/python/pyalps/src/pyalps/math.py | 1 - .../pyalps/src/pyalps/mpl_setup_macosx.py | 17 - .../python/pyalps/src/pyalps/mpl_setup_qt.py | 17 - .../python/pyalps/src/pyalps/mpl_setup_tk.py | 17 - bindings/python/pyalps/src/pyalps/ngs.py | 44 +++ bindings/python/pyalps/src/pyalps/plot.py | 6 +- .../python/pyalps/src/pyalps/plot_core.py | 115 ++++--- bindings/python/pyalps/src/pyalps/tools.py | 39 ++- cmake/UsePyALPS.cmake | 74 +++-- cmake/passthru.py | 12 +- example/alea/example_autocorrelation.py | 1 - example/alea/example_error.py | 1 - example/alea/example_mean.py | 1 - example/alea/example_running_mean.py | 1 - example/alea/example_variance.py | 1 - src/alps/hdf5/archive.cpp | 8 + src/alps/ngs/lib/paramvalue.cpp | 10 +- test/pyalps/accumulators.py | 1 - test/pyalps/loadobs.py | 1 - test/pyalps/mcanalyze.py | 110 ------- test/pyalps/numpylarge.py | 1 - test/pyalps/pyhdf5_test.py | 43 ++- test/pyalps/pyioarchive.py | 28 -- test/pyalps/pyioarchive_test.py | 53 ++++ test/pyalps/pyparams_test.py | 1 - test/pyalps/test_binding_surface.py | 297 ++++++++++++++++-- test/pyalps/test_wheel_payload.py | 153 +++++++++ tool/alea/mcanalyze_tools.py | 1 - tool/maxent.cpp | 6 + tutorials/code-01-python/ising-skeleton.py | 1 - .../o_n_model/experiment/experiment.py | 8 +- .../heisenberg/o_n_model/test/3d3d.py | 2 +- tutorials/code-08-mcmain-python/ising.py | 6 +- tutorials/code-08-mcmain-python/main.py | 3 +- .../code-09-mcmain-python-hybrid/ising.py | 4 +- .../code-09-mcmain-python-hybrid/main.py | 3 +- .../dmft-02-hybridization/tutorial2eval.py | 1 - .../dmft-03-interaction/tutorial3eval.py | 1 - tutorials/dmft-07-hirschfye/tutorial7eval.py | 1 - tutorials/dmft-08-lattices/DOS/DOS_Bethe.py | 28 +- tutorials/dmft-08-lattices/DOS/DOS_Cubic.py | 38 +-- .../dmft-08-lattices/DOS/DOS_Hexagonal.py | 40 +-- tutorials/dmft-08-lattices/DOS/DOS_Square.py | 40 +-- .../build_lattice.py | 1 - .../dmrg-03-ground-state-energies/spin_one.py | 1 - .../spin_one_half.py | 1 - .../spin_one_half_multiple.py | 1 - .../spin_one_multiple.py | 1 - tutorials/dmrg-04-gaps/spin_one_half_gap.py | 1 - .../dmrg-04-gaps/spin_one_half_triplet.py | 1 - .../build_lattice.py | 1 - tutorials/dmrg-06-correlations/spin_one.py | 1 - .../dmrg-06-correlations/spin_one_half.py | 1 - tutorials/ed-01-sparsediag/tutorial1a.py | 1 - tutorials/ed-05-nnn-chain/nnn-crit-pt.py | 1 - tutorials/hybridization-02-kondo/tutorial2.py | 1 - .../tutorial3.py | 1 - .../tutorial4a.py | 1 - .../tutorial4b.py | 1 - tutorials/intro-01-basics/tutorial-binder.py | 1 - .../intro-01-basics/tutorial-evaluate.py | 1 - tutorials/intro-01-basics/tutorial-full.py | 1 - tutorials/intro-01-basics/tutorial-gnuplot.py | 1 - .../intro-01-basics/tutorial-graceplot.py | 1 - .../intro-01-basics/tutorial-magnetization.py | 1 - .../intro-01-basics/tutorial-prepareinput.py | 1 - .../intro-01-basics/tutorial-runsimulation.py | 1 - tutorials/intro-01-basics/tutorial-text.py | 1 - .../tutorial1a.py | 1 - tutorials/mc-04-measurements/tutorial4.py | 1 - tutorials/mc-06-qwl/tutorial6d.py | 1 - .../tutorial8a.py | 1 - tutorials/ngs/6_python_native/ising.py | 6 +- tutorials/ngs/6_python_native/main.py | 6 +- tutorials/ngs/7_python_extend/ising.py | 4 +- tutorials/ngs/7_python_extend/main.py | 6 +- tutorials/notebook/ja/tutorial_ed01a.py | 8 +- tutorials/notebook/ja/tutorial_mc01b.py | 4 +- tutorials/notebook/ja/tutorial_mc04.py | 4 +- tutorials/notebook/ja/tutorial_mc06d.py | 2 +- tutorials/notebook/ja/tutorial_mc08a.py | 2 +- tutorials/test_py.py | 1 - 100 files changed, 1151 insertions(+), 602 deletions(-) mode change 100755 => 100644 bindings/python/pyalps/src/pyalps/lattice.py delete mode 100644 bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py delete mode 100644 bindings/python/pyalps/src/pyalps/mpl_setup_qt.py delete mode 100644 bindings/python/pyalps/src/pyalps/mpl_setup_tk.py delete mode 100644 test/pyalps/mcanalyze.py delete mode 100644 test/pyalps/pyioarchive.py create mode 100644 test/pyalps/pyioarchive_test.py create mode 100644 test/pyalps/test_wheel_payload.py diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index afa818b45..4a6dcff34 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -21,7 +21,6 @@ jobs: - { os: ubuntu-latest, target: "", arch: x86_64 } #- { os: macos-13, target: "13.0" , arch: x86_64 } #DEPRECATED. Too old. - { os: macos-15, target: "15.0" , arch: arm64 } - - { os: macos-26, target: "26.0" , arch: arm64 } steps: - uses: actions/checkout@v7 @@ -67,7 +66,7 @@ jobs: pipx run twine check dist/*.tar.gz tar -tzf dist/*.tar.gz > sdist-manifest.txt for path in _vendor/lib/xml/ALPS.xsl _vendor/tool/maxent.cpp \ - _vendor/applications/dmft/qmc _vendor/applications/qmc/dwa \ + _vendor/applications/dmft/qmc \ LICENSE.txt src/pyalps/__init__.py; do grep -q "$path" sdist-manifest.txt || { echo "missing $path in sdist"; exit 1; } done @@ -83,13 +82,18 @@ jobs: # repaired, uploaded-and-downloaded wheel installed with pip on a clean # runner, at the oldest and newest supported Python. smoke_test: - name: Smoke test wheels on ${{ matrix.os }} / py${{ matrix.python }} + name: Smoke test wheels on ${{ matrix.plat.os }} / py${{ matrix.python }} needs: [build_wheels] - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.plat.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-15, macos-26] + plat: + - { os: ubuntu-latest, artifact_os: ubuntu-latest } + - { os: macos-15, artifact_os: macos-15 } + # The macOS 15 deployment target is forward-compatible; test that + # wheel on the newest runner rather than publishing a second copy. + - { os: macos-26, artifact_os: macos-15 } python: ["3.10", "3.14"] steps: - uses: actions/checkout@v7 @@ -100,7 +104,7 @@ jobs: - uses: actions/download-artifact@v8 with: - pattern: cibw-wheels-* + pattern: cibw-wheels-${{ matrix.plat.artifact_os }}-* path: wheelhouse merge-multiple: true @@ -108,7 +112,7 @@ jobs: run: | pipx run twine check wheelhouse/*.whl python -m pip install numpy scipy pytest - python -m pip install --no-index --no-deps --find-links wheelhouse pyalps + python -m pip install --no-index --find-links wheelhouse pyalps - name: Import and run binding surface tests run: | @@ -140,7 +144,7 @@ jobs: sudo apt-get update sudo apt-get install -y libopenmpi-dev openmpi-bin python -m pip install numpy scipy pytest mpi4py - python -m pip install --no-index --no-deps --find-links wheelhouse pyalps + python -m pip install --no-index --find-links wheelhouse pyalps - name: Run two-rank compatibility surface run: mpiexec -n 2 python -m pytest -q test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface diff --git a/CMakePresets.json b/CMakePresets.json index 288d282e9..98b158bf6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -17,17 +17,18 @@ { "name": "wheel-deps", "displayName": "ALPS C++ SDK for pyalps wheels", - "description": "Libs-only SDK install that the standalone pyalps wheel build links against (ALPS_DIR=_build/wheel-deps/install/share/alps).", + "description": "SDK install that the standalone pyalps wheel build links against (ALPS_DIR=_build/wheel-deps/install/share/alps). Applications are built so the wheel can bundle them into pyalps/bin, as the legacy ALPS_PYTHON_WHEEL build did; MPI is off, matching that build.", "generator": "Ninja", "binaryDir": "${sourceDir}/_build/wheel-deps", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_INSTALL_PREFIX": "${sourceDir}/_build/wheel-deps/install", - "ALPS_BUILD_LIBS_ONLY": "ON", + "ALPS_BUILD_LIBS_ONLY": "OFF", "ALPS_BUILD_TESTS": "OFF", "ALPS_BUILD_EXAMPLES": "OFF", - "ALPS_BUILD_APPLICATIONS": "OFF", - "ALPS_ENABLE_MPI": "OFF" + "ALPS_BUILD_APPLICATIONS": "ON", + "ALPS_ENABLE_MPI": "OFF", + "ALPS_INCLUDE_TUTORIALS": "OFF" } } ], @@ -39,7 +40,9 @@ { "name": "wheel-deps", "configurePreset": "wheel-deps", - "targets": ["install"] + "targets": [ + "install" + ] } ], "testPresets": [ diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index 4faf3af85..edf9dbe65 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -1,7 +1,7 @@ # Copyright (C) 2026 by the ALPS collaboration # SPDX-License-Identifier: MIT -cmake_minimum_required(VERSION 3.18) +cmake_minimum_required(VERSION 3.22) project(pyalps LANGUAGES CXX) option(PYALPS_BUILD_APPLICATIONS "Build optional ALPS application bindings" ON) @@ -60,19 +60,24 @@ set(_pyalps_targets # add FREE_THREADED (libalps relies on the GIL as its lock around # shared state) and do not add STABLE_ABI without revisiting the wheel # matrix — per-version wheels are deliberate. -nanobind_add_module(pyalea_c NB_STATIC "${_bindings}/pyalea.cpp") -nanobind_add_module(pymcdata_c NB_STATIC "${_bindings}/pymcdata.cpp") -nanobind_add_module(pytools_c NB_STATIC "${_bindings}/pytools.cpp") -nanobind_add_module(pyngsparams_c NB_STATIC "${_bindings}/ngs/params.cpp") -nanobind_add_module(pyngshdf5_c NB_STATIC "${_bindings}/ngs/hdf5.cpp") -nanobind_add_module(pyngsbase_c NB_STATIC "${_bindings}/ngs/mcbase.cpp") -nanobind_add_module(pyngsobservable_c NB_STATIC "${_bindings}/ngs/observable.cpp") -nanobind_add_module(pyngsobservables_c NB_STATIC "${_bindings}/ngs/observables.cpp") -nanobind_add_module(pyngsresult_c NB_STATIC "${_bindings}/ngs/result.cpp") -nanobind_add_module(pyngsresults_c NB_STATIC "${_bindings}/ngs/results.cpp") -nanobind_add_module(pyngsapi_c NB_STATIC "${_bindings}/ngs/api.cpp") -nanobind_add_module(pyngsrandom01_c NB_STATIC "${_bindings}/ngs/random01.cpp") -nanobind_add_module(pyngsaccumulator_c NB_STATIC "${_bindings}/ngs/accumulator.cpp") +# NB_STATIC keeps nanobind's core private to each extension. On musllinux, +# MUSL_DYNAMIC_LIBCPP prevents nanobind from also embedding libstdc++ and +# libgcc into every module; auditwheel vendors those shared runtimes once. +set(_pyalps_nanobind_options NB_STATIC MUSL_DYNAMIC_LIBCPP) + +nanobind_add_module(pyalea_c ${_pyalps_nanobind_options} "${_bindings}/pyalea.cpp") +nanobind_add_module(pymcdata_c ${_pyalps_nanobind_options} "${_bindings}/pymcdata.cpp") +nanobind_add_module(pytools_c ${_pyalps_nanobind_options} "${_bindings}/pytools.cpp") +nanobind_add_module(pyngsparams_c ${_pyalps_nanobind_options} "${_bindings}/ngs/params.cpp") +nanobind_add_module(pyngshdf5_c ${_pyalps_nanobind_options} "${_bindings}/ngs/hdf5.cpp") +nanobind_add_module(pyngsbase_c ${_pyalps_nanobind_options} "${_bindings}/ngs/mcbase.cpp") +nanobind_add_module(pyngsobservable_c ${_pyalps_nanobind_options} "${_bindings}/ngs/observable.cpp") +nanobind_add_module(pyngsobservables_c ${_pyalps_nanobind_options} "${_bindings}/ngs/observables.cpp") +nanobind_add_module(pyngsresult_c ${_pyalps_nanobind_options} "${_bindings}/ngs/result.cpp") +nanobind_add_module(pyngsresults_c ${_pyalps_nanobind_options} "${_bindings}/ngs/results.cpp") +nanobind_add_module(pyngsapi_c ${_pyalps_nanobind_options} "${_bindings}/ngs/api.cpp") +nanobind_add_module(pyngsrandom01_c ${_pyalps_nanobind_options} "${_bindings}/ngs/random01.cpp") +nanobind_add_module(pyngsaccumulator_c ${_pyalps_nanobind_options} "${_bindings}/ngs/accumulator.cpp") if(PYALPS_BUILD_APPLICATIONS) if(NOT EXISTS "${_alps_source_root}/tool/maxent.cpp") @@ -83,13 +88,13 @@ if(PYALPS_BUILD_APPLICATIONS) set(_dmft "${_alps_source_root}/applications/dmft/qmc") - nanobind_add_module(maxent_c NB_STATIC + nanobind_add_module(maxent_c ${_pyalps_nanobind_options} "${_alps_source_root}/tool/maxent.cpp" "${_alps_source_root}/tool/maxent_helper.cpp" "${_alps_source_root}/tool/maxent_simulation.cpp" "${_alps_source_root}/tool/maxent_parms.cpp") - nanobind_add_module(cthyb NB_STATIC + nanobind_add_module(cthyb ${_pyalps_nanobind_options} "${_dmft}/hybridization/hybmain.cpp" "${_dmft}/hybridization/hybsim.cpp" "${_dmft}/hybridization/hyblocal.cpp" @@ -103,7 +108,7 @@ if(PYALPS_BUILD_APPLICATIONS) "${_dmft}/hybridization/hybevaluate.cpp" "${_dmft}/hybridization/hybmeasurements.cpp") - nanobind_add_module(ctint NB_STATIC + nanobind_add_module(ctint ${_pyalps_nanobind_options} "${_dmft}/interaction_expansion2/main.cpp" "${_dmft}/fouriertransform.C" "${_dmft}/interaction_expansion2/auxiliary.cpp" @@ -117,14 +122,11 @@ if(PYALPS_BUILD_APPLICATIONS) "${_dmft}/interaction_expansion2/measurements.cpp" "${_dmft}/interaction_expansion2/model.cpp") - nanobind_add_module(dwa_c NB_STATIC "${_bindings}/apps/dwa.cpp") - - list(APPEND _pyalps_targets maxent_c cthyb ctint dwa_c) + list(APPEND _pyalps_targets maxent_c cthyb ctint) foreach(_target IN ITEMS maxent_c cthyb ctint) target_compile_definitions(${_target} PRIVATE BUILD_PYTHON_MODULE) target_include_directories(${_target} PRIVATE "${_bindings}" "${_dmft}") endforeach() - target_include_directories(dwa_c PRIVATE "${_alps_source_root}/applications/qmc/dwa") endif() # Compile the bindings with the same preprocessor configuration the @@ -141,6 +143,35 @@ foreach(_flag IN LISTS _alps_sdk_cxx_flags) endif() endforeach() +option(PYALPS_BUNDLE_APPLICATIONS + "Bundle the ALPS application executables into pyalps/bin" ON) + +# Where the extension modules look for libalps at run time. +# +# When the ALPS libraries ship inside the package (the default), the *only* +# route to libalps must be the in-package pyalps/lib copy -- the same one the +# bundled programs load. Leaving the SDK's own library directory on the list +# as well is not a harmless fallback: auditwheel/delocate resolve the first +# match, find libalps outside the wheel, and vendor a second copy into +# pyalps.libs (or pyalps/.dylibs), so the wheel carries the same 10 MB library +# twice. HDF5 stays on the list precisely because it *should* be vendored. +# +# With PYALPS_BUNDLE_APPLICATIONS=OFF nothing installs libalps into the +# package, so there the SDK directory is the only way for the repair tools to +# find it at all. +if(APPLE) + set(_pyalps_in_package_libs "@loader_path/../lib") +else() + set(_pyalps_in_package_libs "$ORIGIN/../lib") +endif() +if(PYALPS_BUNDLE_APPLICATIONS) + set(_pyalps_install_rpath + "${_pyalps_in_package_libs};${_alps_hdf5_prefix}/lib") +else() + set(_pyalps_install_rpath + "${ALPS_LIBRARY_DIRS};${_alps_hdf5_prefix}/lib") +endif() + foreach(_target IN LISTS _pyalps_targets) if(_alps_sdk_definitions) target_compile_definitions(${_target} PRIVATE ${_alps_sdk_definitions}) @@ -151,7 +182,7 @@ foreach(_target IN LISTS _pyalps_targets) target_link_libraries(${_target} PRIVATE ${_pyalps_link_libraries}) target_link_options(${_target} PRIVATE ${_pyalps_link_options}) set_target_properties(${_target} PROPERTIES - INSTALL_RPATH "${ALPS_LIBRARY_DIRS};${_alps_hdf5_prefix}/lib") + INSTALL_RPATH "${_pyalps_install_rpath}") endforeach() set(_extension_dir "pyalps/_ext") @@ -171,6 +202,95 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/pyalps_config.py.in" "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" DESTINATION pyalps) +# The ALPS application executables ship inside the package (pyalps/bin), as +# the legacy ALPS_PYTHON_WHEEL build did. Keep this list explicit: the SDK's +# bin directory also contains conversion and plotting tools whose relative +# data paths assume a prefix install rather than the wheel layout. +# pyalps.tools.check_existence() still prepends /bin to PATH, so +# without these files runApplication('spinmc', ...) fails on a wheel install +# with "There is no spinmc on the path!" -- the fallback it tries instead, +# pyalps_config.ALPS_BIN_INSTALL_DIR, is the build machine's SDK path. +# +# The programs are copied from the SDK rather than rebuilt here. The root +# CMakeLists gives every installed target an RPATH of @loader_path/../lib +# (APPLE) or $ORIGIN/../lib, so they resolve libalps out of pyalps/lib below +# without any post-install patching -- the same directory the extension +# modules point at, so one copy of each library serves both. +if(PYALPS_BUNDLE_APPLICATIONS) + set(_alps_bin_dir "${ALPS_ROOT_DIR}/bin") + set(_alps_application_names + checksign + dirloop_sse + dmft + dmrg + fulldiag + fulldiag_evaluate + hirschfye + hybridization + interaction + loop + qwl + qwl_evaluate + simplemc + sparsediag + spinmc + spinmc_evaluate + worm + worm_evaluate) + set(_alps_programs "") + set(_alps_missing_programs "") + foreach(_program IN LISTS _alps_application_names) + if(EXISTS "${_alps_bin_dir}/${_program}") + list(APPEND _alps_programs "${_alps_bin_dir}/${_program}") + else() + list(APPEND _alps_missing_programs "${_program}") + endif() + endforeach() + if(_alps_missing_programs) + message(FATAL_ERROR + "PYALPS_BUNDLE_APPLICATIONS is ON but the wheel SDK is missing these " + "legacy wheel applications: ${_alps_missing_programs}. Build the ALPS " + "SDK with the wheel-deps preset, or configure with " + "-DPYALPS_BUNDLE_APPLICATIONS=OFF for a bindings-only wheel -- in which " + "case pyalps.runApplication() will not work.") + endif() + list(LENGTH _alps_programs _alps_program_count) + message(STATUS "pyalps: bundling ${_alps_program_count} ALPS programs into pyalps/bin") + install(PROGRAMS ${_alps_programs} DESTINATION pyalps/bin) + + # The bundled programs resolve libalps and libboost through their ../lib + # RPATH, so the SDK's shared libraries have to travel with them -- each one + # exactly once. install(DIRECTORY) is the wrong tool here: it would copy the + # whole SONAME chain (libalps.so, libalps.so.2, libalps.so.2.3.4), and since + # a wheel cannot carry symlinks those arrive as three byte-identical 10 MB + # files. Install only the SONAME-named file, which is the name every + # DT_NEEDED / LC_LOAD_DYLIB entry actually asks for. + # + # src/alps/CMakeLists.txt gives libalps SOVERSION ${ALPS_VERSION_MAJOR} on + # ELF platforms and no version at all on APPLE, so "." is the + # SONAME spelling wherever a versioned form exists. test/pyalps guards both + # halves of this: that no library is bundled twice, and that every bundled + # program can still be loaded. + file(GLOB _alps_runtime_libs LIST_DIRECTORIES FALSE + "${ALPS_ROOT_DIR}/lib/*.so" "${ALPS_ROOT_DIR}/lib/*.dylib") + if(NOT _alps_runtime_libs) + message(FATAL_ERROR + "PYALPS_BUNDLE_APPLICATIONS is ON but no shared libraries were found in " + "${ALPS_ROOT_DIR}/lib. The bundled programs would ship without the " + "libalps they link against.") + endif() + foreach(_lib IN LISTS _alps_runtime_libs) + get_filename_component(_lib_name "${_lib}" NAME) + set(_lib_soname "${_lib_name}") + if(EXISTS "${_lib}.${ALPS_VERSION_MAJOR}") + set(_lib_soname "${_lib_name}.${ALPS_VERSION_MAJOR}") + endif() + get_filename_component(_lib_real "${_lib}" REALPATH) + install(PROGRAMS "${_lib_real}" DESTINATION pyalps/lib + RENAME "${_lib_soname}") + endforeach() +endif() + # The ALPS XML/XSL library ships inside the package (pyalps/xml), as the # legacy wheel build did: the stylesheets plus the lattice and model # libraries that parameter files reference by default. diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index e25ab8a98..d0d52e959 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -33,9 +33,20 @@ with `python -m pip install`. With ccache installed, configure with speed up rebuilds. `PYALPS_BUILD_APPLICATIONS=ON` is the default and preserves the MaxEnt, -DWA, CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake +CT-HYB, and CT-INT extension modules. Set it to `OFF` through CMake configuration for a smaller core-only developer build. +`PYALPS_BUNDLE_APPLICATIONS=ON` is the default and copies the ALPS +application executables (`spinmc`, `dmrg`, `sparsediag`, `loop`, `qwl`, ...) +from the SDK into `pyalps/bin`, together with the SDK's shared libraries in +`pyalps/lib` that their `../lib` RPATH resolves against. `pyalps.tools` +prepends `pyalps/bin` to `PATH`, so this is what makes +`pyalps.runApplication('spinmc', ...)` work from a wheel install — the +`wheel-deps` preset therefore builds the applications. Configure with +`-DPYALPS_BUNDLE_APPLICATIONS=OFF` for a bindings-only wheel; the +`runApplication` helpers then require the executables on `PATH` by other +means. + ## Free-threading and stable-ABI policy pyalps ships per-version wheels (CPython 3.10–3.14) and deliberately opts @@ -49,8 +60,8 @@ into neither of nanobind's special ABI modes: singleton, `mcdata`'s lazily-computed statistics). Do not add `FREE_THREADED` to `nanobind_add_module` without first making that state thread-safe. -- **Stable ABI (abi3):** not enabled or currently supported. Some binding - paths still inspect CPython type internals (`tp_name`), and no abi3 build - runs in CI. Per-version wheels are deliberate; do not add `STABLE_ABI` - until the code is limited-API clean and CI compiles and imports the - resulting extensions. +- **Stable ABI (abi3):** not enabled. Nanobind isolates stable-ABI and + ordinary extensions from each other. ALPS supports downstream nanobind + modules that derive from pyalps types, so an abi3 pyalps wheel would force + every such consumer to use the limited API too. Per-version wheels preserve + ordinary downstream extension interoperability. diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 616865ff8..1121bd6b8 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -15,12 +15,23 @@ #include #include #include +// nanobind builds extensions with -fvisibility=hidden +// (CXX_VISIBILITY_PRESET hidden). A hidden type_info cannot be merged with +// libalps' own copy, and a catch clause only matches when the two agree -- +// so the register_exception_translator below silently failed to catch any +// alps::hdf5::* exception and every archive failure reached Python as a bare +// RuntimeError carrying the whole ALPS_STACKTRACE, with +// pyalps.hdf5.ArchiveNotFound and friends never raised. The legacy +// Boost.Python modules were built with default visibility, which is why the +// same translator worked there. Give ALPS' types default visibility here. +#pragma GCC visibility push(default) #include #include #include #include #include #include +#pragma GCC visibility pop #include "extract_from_pyobject.hpp" #include "../numpy_compat.hpp" #include @@ -362,9 +373,42 @@ namespace alps { std::string python_hdf5_get_filename(alps::hdf5::archive & ar) { return ar.get_filename(); } + // Does `data` expose a save() written in Python (as opposed to one + // inherited from a bound C++ type)? The legacy build dispatched to + // obj.save(archive) here, but gated it on the bound method's type + // name being "instancemethod" -- a Python 2 spelling, so the branch + // was dead on Python 3 and `ar["/"] = simulation` raised + // "Unsupported type" instead of checkpointing the object. Gate on + // types.MethodType instead, which is the Python 3 equivalent and, + // like the original, does not match nanobind's own method objects -- + // so registered extension types keep their native save path. + bool has_python_save_method(nb::handle data) { + if (!nb::hasattr(data, "save")) + return false; + nb::object attr = nb::getattr(data, "save"); + // A bound method defined in Python has tp_name "method"; the + // legacy code compared against "instancemethod", the Python 2 + // spelling. Compare the type name rather than calling + // PyMethod_Check: nanobind links extensions against a restricted + // CPython symbol list that does not export PyMethod_Type. This + // also matches the is_ndarray() check above. + return std::strcmp(Py_TYPE(attr.ptr())->tp_name, "method") == 0; + } void python_hdf5_save(alps::hdf5::archive & ar, std::string const & path, nb::handle data) { + if (has_python_save_method(data)) { + std::string context = ar.get_context(); + ar.set_context(ar.complete_path(path)); + try { + nb::getattr(data, "save")(nb::cast(&ar, nb::rv_policy::reference)); + } catch (...) { + ar.set_context(context); + throw; + } + ar.set_context(context); + return; + } hdf5_save_py11_visitor visitor{ar, path}; extract_from_pyobject_py11(visitor, data); } diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index bde258c0e..5cb421eb4 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -130,8 +130,17 @@ NB_MODULE(pyngsbase_c, m) { .def("update", &alps::mcbase::update) .def("measure", &alps::mcbase::measure) .def("fraction_completed", &alps::mcbase::fraction_completed) - .def("save", static_cast( - &alps::mcbase::save)) - .def("load", static_cast( - &alps::mcbase::load)); + // Bind the BASE implementations with qualified calls so that they do + // not dispatch virtually. Binding &alps::mcbase::save as a + // pointer-to-member dispatches through the vtable, so a Python + // subclass that overrode save() and then called + // ngs.mcbase.save(self, ar) -- or super().save(ar) -- re-entered its + // own override and ran the body twice. Overriding subclasses still + // reach C++ through the trampoline's NB_OVERRIDE, which is unaffected. + .def("save", [](alps::mcbase const & self, alps::hdf5::archive & ar) { + self.alps::mcbase::save(ar); + }) + .def("load", [](alps::mcbase & self, alps::hdf5::archive & ar) { + self.alps::mcbase::load(ar); + }); } diff --git a/bindings/python/pyalps/cpp/ngs/observable.cpp b/bindings/python/pyalps/cpp/ngs/observable.cpp index 8c653517b..537aca696 100644 --- a/bindings/python/pyalps/cpp/ngs/observable.cpp +++ b/bindings/python/pyalps/cpp/ngs/observable.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -61,11 +62,36 @@ namespace alps { self.load(ar); ar.set_context(current); } + // Construct the underlying alea observable inside libalps rather than + // in this translation unit. + // + // nanobind compiles extensions with -fvisibility=hidden + // (CXX_VISIBILITY_PRESET hidden). Instantiating a libalps class + // template here therefore emits a *hidden* vtable and type_info for it, + // which the dynamic loader cannot merge with libalps' own copy. The + // object then carries this module's RTTI, and the + // dynamic_cast*> that libalps performs in + // Observable::add (src/alps/alea/observable.h:161) fails, so appending a + // sample raised "Cannot add measurement to observable ". + // + // The legacy Boost.Python modules were built with default visibility + // (cmake/FindPythonMod.cmake's PYTHON_ADD_MODULE set no visibility + // flag), so the two type_infos merged and the same code worked there. + // + // Routing through mcobservables::create_RealObservable keeps every + // construction, clone and cast on the libalps side of the boundary. + // Do NOT "simplify" this back to constructing alps::RealObservable + // here. The temporary set owns the observable until the copy taken on + // return bumps its reference count. alps::mcobservable create_RealObservable_export(std::string name) { - return alps::mcobservable(std::make_shared(name).get()); + alps::mcobservables set; + set.create_RealObservable(name); + return set[name]; } alps::mcobservable create_RealVectorObservable_export(std::string name) { - return alps::mcobservable(std::make_shared(name).get()); + alps::mcobservables set; + set.create_RealVectorObservable(name); + return set[name]; } } } diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index e6d30d9c7..ac02e8559 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -97,18 +97,12 @@ NB_MODULE(pyngsobservables_c, m) { return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); }, nb::keep_alive<0, 1>()) - .def("keys", [](alps::mcobservables & self) { - return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) - .def("values", [](alps::mcobservables & self) { - return nb::make_value_iterator(nb::type(), "value_iterator", self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) - .def("items", [](alps::mcobservables & self) { - return nb::make_iterator(nb::type(), "item_iterator", self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) + // keys/values/items are deliberately NOT defined here. Boost.Python's + // map_indexing_suite did not define them either, so they resolved through + // MutableMapping to set-like KeysView/ValuesView/ItemsView. Defining them + // natively as nanobind iterators would narrow that surface (no len(), no + // set operators, exhausted after one pass) and pyalps/ngs.py cannot + // recover it -- its guard skips any name the C++ class already provides. .def("reset", &alps::mcobservables::reset, nb::arg("equilibrated") = false) .def("save", &alps::mcobservables::save) .def("load", &mcobservables_load) diff --git a/bindings/python/pyalps/cpp/ngs/results.cpp b/bindings/python/pyalps/cpp/ngs/results.cpp index 1b4b7d8ad..6f4738630 100644 --- a/bindings/python/pyalps/cpp/ngs/results.cpp +++ b/bindings/python/pyalps/cpp/ngs/results.cpp @@ -59,27 +59,12 @@ NB_MODULE(pyngsresults_c, m) { self.begin(), self.end()); }, nb::keep_alive<0, 1>()) - .def("keys", [](alps::mcresults & self) { - return nb::make_key_iterator( - nb::type(), - "key_iterator", - self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) - .def("values", [](alps::mcresults & self) { - return nb::make_value_iterator( - nb::type(), - "value_iterator", - self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) - .def("items", [](alps::mcresults & self) { - return nb::make_iterator( - nb::type(), - "item_iterator", - self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) + // keys/values/items are deliberately NOT defined here. Boost.Python's + // map_indexing_suite did not define them either, so they resolved through + // MutableMapping to set-like KeysView/ValuesView/ItemsView. Defining them + // natively as nanobind iterators would narrow that surface (no len(), no + // set operators, exhausted after one pass) and pyalps/ngs.py cannot + // recover it -- its guard skips any name the C++ class already provides. .def("__str__", &alps::detail::mcresults_print) .def("save", &alps::mcresults::save) .def("load", &alps::detail::mcresults_load); diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index c9195f15a..802645978 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -65,7 +65,6 @@ ALPS_DIR = { env = "ALPS_DIR" } [tool.scikit-build.sdist.force-include] "../../../LICENSE.txt" = "LICENSE.txt" "../../../applications/dmft/qmc" = "_vendor/applications/dmft/qmc" -"../../../applications/qmc/dwa" = "_vendor/applications/qmc/dwa" "../../../tool" = "_vendor/tool" "../../../lib/xml" = "_vendor/lib/xml" diff --git a/bindings/python/pyalps/src/pyalps/__init__.py b/bindings/python/pyalps/src/pyalps/__init__.py index 250e36db6..b3066bb35 100644 --- a/bindings/python/pyalps/src/pyalps/__init__.py +++ b/bindings/python/pyalps/src/pyalps/__init__.py @@ -1,15 +1,14 @@ -from __future__ import absolute_import # **************************************************************************** -# +# # ALPS Project: Algorithms and Libraries for Physics Simulations -# +# # ALPS Libraries -# +# # Copyright (C) 1994-2009 by Bela Bauer -# +# # ALPS Project: https://alps.comp-phys.org/ # SPDX-License-Identifier: MIT -# +# # **************************************************************************** import sys diff --git a/bindings/python/pyalps/src/pyalps/alea.py b/bindings/python/pyalps/src/pyalps/alea.py index 9b44c75e7..f8fe8a58b 100644 --- a/bindings/python/pyalps/src/pyalps/alea.py +++ b/bindings/python/pyalps/src/pyalps/alea.py @@ -23,12 +23,10 @@ - MCScalarTimeseriesView - MCVectorTimeseriesView """ -from __future__ import print_function -from __future__ import absolute_import from .cxx.pymcdata_c import * from .cxx.pyalea_c import RealObservable, RealVectorObservable, RealTimeSeriesObservable, RealVectorTimeSeriesObservable -from .cxx.pyalea_c import MCScalarTimeseries, MCScalarTimeseriesView, MCVectorTimeseries, MCVectorTimeseries, ValueWithError, StdPairDouble, size, mean, variance, integrated_autocorrelation_time, running_mean, reverse_running_mean +from .cxx.pyalea_c import MCScalarTimeseries, MCScalarTimeseriesView, MCVectorTimeseries, MCVectorTimeseriesView, ValueWithError, StdPairDouble, size, mean, variance, integrated_autocorrelation_time, running_mean, reverse_running_mean from . import alea_detail as detail import numpy import pyalps.dataset @@ -52,9 +50,9 @@ def autocorrelation(timeseries, _distance = None, _limit = None): _distance: Calculates the autocorrelation until a specific length\n\ _limit: Calculates the autocorrelation until it has reached _limit of its initial value\n\ returns: MCTimeseries object with the autocorrelation" - if _distance != None: + if _distance is not None: return detail.autocorrelation_distance(timeseries, _distance) - if _limit != None: + if _limit is not None: return detail.autocorrelation_limit(timeseries, _limit) print("Usage: autocorrelation(timeseries, [_distance = XXX | _limit = XXX] )") @@ -65,9 +63,9 @@ def cut_head(timeseries, _distance = None, _limit = None): _limit: Cuts the front until the timeseries reaches _limit of its initial value\n\ returns: MCTimeseriesView object with the smaller timeseries\n\ Note: does not copy the data, only creates a reference." - if _distance != None: + if _distance is not None: return detail.cut_head_distance(timeseries, _distance) - if _limit != None: + if _limit is not None: return detail.cut_head_limit(timeseries, _limit) print("Usage: cut_head(timeseries, [_distance = XXX | _limit = XXX] )") @@ -78,9 +76,9 @@ def cut_tail(timeseries, _distance = None, _limit = None): _limit: Cuts the tail until the timeseries only decays from its initial value to _limit of its initial value\n\ returns: MCTimeseriesView object with the smaller timeseries\n\ Note: does not copy the data, only creates a reference." - if _distance != None: + if _distance is not None: return detail.cut_tail_distance(timeseries, _distance) - if _limit != None: + if _limit is not None: return detail.cut_tail_limit(timeseries, _limit) print("Usage: cut_head(timeseries, [_distance = XXX | _limit = XXX] )") @@ -91,9 +89,9 @@ def exponential_autocorrelation_time(autocorrelation, _from = None, _to = None, _max & _min: fits the autocorrelation between the values where it is at _max and where it is at _min from its initial value\n\ returns: StdPairDouble object FIT with the parameters of the fit\n\ Note: The equation is FIT.first * exp(FIT.second * t)" - if (_from != None and _to != None): + if (_from is not None and _to is not None): return detail.exponential_autocorrelation_time_distance(autocorrelation, _from, _to) - if (_max != None and _min != None): + if (_max is not None and _min is not None): return detail.exponential_autocorrelation_time_limit(autocorrelation, _max, _min) print("Usage: exponential_autocorrelation_time(autocorrelation, [_from = XXX, _to = XXX | _max = XXX, _min = XXX] )") diff --git a/bindings/python/pyalps/src/pyalps/apptest.py b/bindings/python/pyalps/src/pyalps/apptest.py index b15c59a44..9ec2a66fd 100644 --- a/bindings/python/pyalps/src/pyalps/apptest.py +++ b/bindings/python/pyalps/src/pyalps/apptest.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/bindings/python/pyalps/src/pyalps/cxx.py b/bindings/python/pyalps/src/pyalps/cxx.py index 72784f3e2..f18d50da5 100644 --- a/bindings/python/pyalps/src/pyalps/cxx.py +++ b/bindings/python/pyalps/src/pyalps/cxx.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/bindings/python/pyalps/src/pyalps/floatwitherror.py b/bindings/python/pyalps/src/pyalps/floatwitherror.py index 21573cbb6..844f10c23 100644 --- a/bindings/python/pyalps/src/pyalps/floatwitherror.py +++ b/bindings/python/pyalps/src/pyalps/floatwitherror.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -33,21 +32,23 @@ def get_error(f): class FloatWithError: - def __init__(self,mean_=0,error_=0,jackknife=[],binsize=0,timeseries=[]): + def __init__(self,mean_=0,error_=0,jackknife=None,binsize=0,timeseries=()): self.mean = mean_ self.error = error_ - self.jackknife = jackknife + # `jackknife=[]` as a default shared one list across every instance + # that did not pass the argument. + self.jackknife = [] if jackknife is None else jackknife self.binsize = binsize self.timeseries = list(timeseries) try: self.shape = self.mean.shape - except AttributeError as ValueError: + # NB: `except AttributeError as ValueError` bound the caught exception + # to the name ValueError, shadowing the builtin for the rest of the scope. + except AttributeError: pass def __str__(self): return str(self.mean) + ' +/- ' + str(self.error) - def __expr__(self): - return expr(self.mean) + ' +/- ' + expr(self.error) def __repr__(self): return self.__str__() diff --git a/bindings/python/pyalps/src/pyalps/hlist.py b/bindings/python/pyalps/src/pyalps/hlist.py index 2212eb43b..306ce0b2a 100644 --- a/bindings/python/pyalps/src/pyalps/hlist.py +++ b/bindings/python/pyalps/src/pyalps/hlist.py @@ -50,7 +50,7 @@ def flatten(sl, fdepth = None): The parameters are the hierachical list, and optionally a depth at which the flattening should happen, to keep, e.g. the top-most structure. """ - if fdepth == None: + if fdepth is None: fdepth = depth(list(sl)) return HList(list(sl), fdepth) @@ -65,27 +65,25 @@ def copy_structure(sl): return 1 def happly(functor, sl, fdepth = None, params = None): - if fdepth == None: + if fdepth is None: fdepth = depth(sl) hl = HList(sl, fdepth) hl.apply(functor, params) def hmap(functor, sl, fdepth = None, params = None): - if fdepth == None: + if fdepth is None: fdepth = depth(sl) hl = HList(sl, fdepth) return hl.map(functor, params) class HList: - def __init__(self): - self.data_ = [] - self.indices_ = [] - + # NB: a second, argument-less __init__ used to be defined above this one + # and was silently shadowed by it, so HList() never worked. def __init__(self,init,fdepth = None): self.data_ = init self.indices_ = [] - if fdepth == None: + if fdepth is None: fdepth = depth(self.data_) if fdepth < 0: fdepth = depth(self.data_) + fdepth @@ -123,7 +121,7 @@ def data(self): def apply(self, functor, params = None): for idx in self.indices_: - if params == None: + if params is None: self[idx] = functor(self[idx]) else: self[idx] = functor(self[idx], params) @@ -132,7 +130,7 @@ def map(self, functor, params = None): ret = copy_structure(self.data_) rethl = HList(ret) for idx in self.indices_: - if params == None: + if params is None: rethl[idx] = functor(self[idx]) else: rethl[idx] = functor(self[idx], params) diff --git a/bindings/python/pyalps/src/pyalps/lattice.py b/bindings/python/pyalps/src/pyalps/lattice.py old mode 100755 new mode 100644 index 92a2ee8d0..d65841d97 --- a/bindings/python/pyalps/src/pyalps/lattice.py +++ b/bindings/python/pyalps/src/pyalps/lattice.py @@ -50,7 +50,7 @@ def showgraph(graph): if(dimension > 2): raise RuntimeError('This function only supports 1 and 2 dimensional lattices.') - if(len(vertices.values()[0]) == 1): + if(len(next(iter(vertices.values()))) == 1): vertices = {k: (v[0],0) for k, v in vertices.items()} x = [v[0] for v in vertices.values()] diff --git a/bindings/python/pyalps/src/pyalps/load.py b/bindings/python/pyalps/src/pyalps/load.py index fd7f201f4..63a6a33b8 100644 --- a/bindings/python/pyalps/src/pyalps/load.py +++ b/bindings/python/pyalps/src/pyalps/load.py @@ -1,5 +1,3 @@ -from __future__ import print_function -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -54,7 +52,7 @@ def parse_labels(labels): v = parse_label(x) larr.append(v) if '--' in x: - if first==None: + if first is None: first = v[0] else: if first != v[0] or len(v) != 2: @@ -198,7 +196,7 @@ def GetIterations(self, current_path, params={}, measurements=None, index=None, respath = current_path+'/iteration/'+it+'/results' list_ = self.GetObservableList(respath) - if measurements == None: + if measurements is None: obslist = list_ else: obslist = [pt.hdf5_name_encode(obs) for obs in measurements if pt.hdf5_name_encode(obs) in list_] @@ -212,7 +210,7 @@ def GetIterations(self, current_path, params={}, measurements=None, index=None, measurements_props = {} measurements_props['hdf5_path'] = itresultspath measurements_props['observable'] = pt.hdf5_name_decode(m) - if index == None: + if index is None: d.y = self.h5f[itresultspath+'/mean/value'] d.x = np.arange(0,len(d.y)) else: @@ -245,7 +243,7 @@ def ReadDiagDataFromFile(self,flist,proppath='/parameters',respath='/spectrum', params = self.ReadParameters(proppath) if 'results' in self.h5f.list_children(respath): list_ = self.GetObservableList(respath+'/results') - if measurements == None: + if measurements is None: obslist = list_ else: obslist = [pt.hdf5_name_encode(obs) for obs in measurements if pt.hdf5_name_encode(obs) in list_] @@ -261,7 +259,7 @@ def ReadDiagDataFromFile(self,flist,proppath='/parameters',respath='/spectrum', secresultspath = respath+'/results/'+m d.props['hdf5_path'] = secresultspath d.props['observable'] = pt.hdf5_name_decode(m) - if index == None: + if index is None: d.y = self.h5f[secresultspath+'/mean/value'] d.x = np.arange(0,len(d.y)) else: @@ -283,7 +281,7 @@ def ReadDiagDataFromFile(self,flist,proppath='/parameters',respath='/spectrum', fileset.append(self.GetIterations(respath, params, measurements, index, verbose)) if 'sectors' in self.h5f.list_children(respath): list_ = self.GetObservableList(respath+'/sectors/0/results') - if measurements == None: + if measurements is None: obslist = list_ else: obslist = [pt.hdf5_name_encode(obs) for obs in measurements if pt.hdf5_name_encode(obs) in list_] @@ -298,7 +296,7 @@ def ReadDiagDataFromFile(self,flist,proppath='/parameters',respath='/spectrum', secresultspath = respath+'/sectors/'+secnum+'/results/'+m d.props['hdf5_path'] = secresultspath d.props['observable'] = pt.hdf5_name_decode(m) - if index == None: + if index is None: d.y = self.h5f[secresultspath+'/mean/value'] d.x = np.arange(0,len(d.y)) else: @@ -341,14 +339,14 @@ def ReadBinningAnalysis(self,flist,measurements=None,proppath='/parameters',resp if verbose: log( 'loading from file ' +f) self.h5f = h5.archive(f, 'r') self.h5fname = f - if respath == None: + if respath is None: respath="/simulation/results" list_ = self.GetObservableList(respath) # this is exception-safe in the sense that it's also required in the line above #grp = self.h5f.require_group(respath) params = self.ReadParameters(proppath) obslist = [] - if measurements == None: + if measurements is None: obslist = list_ else: obslist = [pt.hdf5_name_encode(obs) for obs in measurements if pt.hdf5_name_encode(obs) in list_] @@ -397,7 +395,7 @@ def ReadMeasurementFromFile(self,flist,proppath='/parameters',respath='/simulati list_ = self.GetObservableList(respath) params = self.ReadParameters(proppath) obslist = [] - if measurements == None: + if measurements is None: obslist = list_ else: obslist = [pt.hdf5_name_encode(obs) for obs in measurements if pt.hdf5_name_encode(obs) in list_] diff --git a/bindings/python/pyalps/src/pyalps/math.py b/bindings/python/pyalps/src/pyalps/math.py index fbf0d9b80..d1d0b9c66 100644 --- a/bindings/python/pyalps/src/pyalps/math.py +++ b/bindings/python/pyalps/src/pyalps/math.py @@ -12,7 +12,6 @@ # # **************************************************************************** -from __future__ import absolute_import import numpy as np from pyalps.alea import * import math as pm diff --git a/bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py b/bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py deleted file mode 100644 index 4618f7980..000000000 --- a/bindings/python/pyalps/src/pyalps/mpl_setup_macosx.py +++ /dev/null @@ -1,17 +0,0 @@ -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2009-2010 by Bela Bauer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - -import matplotlib -matplotlib.use('macosx') -import matplotlib.pyplot - diff --git a/bindings/python/pyalps/src/pyalps/mpl_setup_qt.py b/bindings/python/pyalps/src/pyalps/mpl_setup_qt.py deleted file mode 100644 index 7e4719a48..000000000 --- a/bindings/python/pyalps/src/pyalps/mpl_setup_qt.py +++ /dev/null @@ -1,17 +0,0 @@ -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2009-2010 by Bela Bauer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - -import matplotlib -matplotlib.use('Qt4Agg') -import matplotlib.pyplot - diff --git a/bindings/python/pyalps/src/pyalps/mpl_setup_tk.py b/bindings/python/pyalps/src/pyalps/mpl_setup_tk.py deleted file mode 100644 index b0a605138..000000000 --- a/bindings/python/pyalps/src/pyalps/mpl_setup_tk.py +++ /dev/null @@ -1,17 +0,0 @@ -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2009-2010 by Bela Bauer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - -import matplotlib -matplotlib.use('Tk4Agg') -import matplotlib.pyplot - diff --git a/bindings/python/pyalps/src/pyalps/ngs.py b/bindings/python/pyalps/src/pyalps/ngs.py index 516527cf5..1cbe3dfed 100644 --- a/bindings/python/pyalps/src/pyalps/ngs.py +++ b/bindings/python/pyalps/src/pyalps/ngs.py @@ -57,6 +57,50 @@ def addToObservables(self, observables): #rename this with new ALEA if getattr(_mapping_type, _method, None) is getattr(object, _method, None): setattr(_mapping_type, _method, getattr(MutableMapping, _method)) +# Two mixin methods cannot simply be copied onto these types. +# +# MutableMapping.pop reads `self.__marker`, which name-mangles to +# self._MutableMapping__marker -- a class attribute of MutableMapping. Under +# the old `params.__bases__ = (MutableMapping,) + ...` rebasing that resolved +# through the MRO; on a virtual subclass whose methods were copied it does +# not, so pop() raised AttributeError instead of returning the default or +# raising KeyError. Supply an implementation that tests membership instead of +# relying on __getitem__ raising. +# +# alps::params compounds this: its __getitem__ returns None for an undefined +# key rather than raising KeyError (inherited from the Boost.Python module and +# pinned by test/pyalps/pyparams_test.py), so get() ignored its default and +# setdefault() returned None while storing nothing. +_MAPPING_POP_MARKER = object() + + +def _mapping_pop(self, key, default=_MAPPING_POP_MARKER): + if key in self: + value = self[key] + del self[key] + return value + if default is _MAPPING_POP_MARKER: + raise KeyError(key) + return default + + +def _params_get(self, key, default=None): + return self[key] if key in self else default + + +def _params_setdefault(self, key, default=None): + if key in self: + return self[key] + self[key] = default + return default + + +for _mapping_type in (params, observables, results): + _mapping_type.pop = _mapping_pop + +params.get = _params_get +params.setdefault = _params_setdefault + from .cxx.pyngsbase_c import mcbase from .cxx.pyngsapi_c import collectResults, saveResults diff --git a/bindings/python/pyalps/src/pyalps/plot.py b/bindings/python/pyalps/src/pyalps/plot.py index 2f847abde..48cd61a08 100644 --- a/bindings/python/pyalps/src/pyalps/plot.py +++ b/bindings/python/pyalps/src/pyalps/plot.py @@ -1,5 +1,3 @@ -from __future__ import print_function -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -144,7 +142,7 @@ def plot3D(sets, centeredAtOrigin=False, layer=None): X,Y = np.meshgrid(x,y) if len(shape) == 3: - if layer == None: # column integrate + if layer is None: # column integrate Z = np.sum(Z, axis=2); elif layer == "center": Z = Z[:,:,shape[2]//2] @@ -214,7 +212,7 @@ def draw_lines(self): else: self.lines.append([plt.scatter(xmeans, ymeans, marker=thismarker, c=thiscolor, facecolors='none')]) - if xerrors != None or yerrors != None: + if xerrors is not None or yerrors is not None: plt.errorbar(xmeans, ymeans, yerr=yerrors, xerr=xerrors, fmt=None) else: line_props = '-' diff --git a/bindings/python/pyalps/src/pyalps/plot_core.py b/bindings/python/pyalps/src/pyalps/plot_core.py index bcab2c4b8..bd6b9df45 100644 --- a/bindings/python/pyalps/src/pyalps/plot_core.py +++ b/bindings/python/pyalps/src/pyalps/plot_core.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -34,9 +33,9 @@ def read_xml(filename): for point in list(root.find('set')): x.append(float(point.find('x').text)) y.append(float(point.find('y').text)) - if point.find('dx') != None: + if point.find('dx') is not None: x[-1] = fwe(x[-1],float(point.find('dx').text)) - if point.find('dy') != None: + if point.find('dy') is not None: y[-1] = fwe(y[-1],float(point.find('dy').text)) data.x = np.array(x) @@ -50,37 +49,37 @@ def read_xml(filename): def Axis(label=None,mmin=None,mmax=None,log=False): d = {} - if label != None: + if label is not None: d['label'] = label - if mmin != None: + if mmin is not None: d['min'] = min - if mmax != None: + if mmax is not None: d['max'] = max - if log != None: + if log is not None: d['log'] = log return d def Legend(location=None): d = {} - if location != None: + if location is not None: d['location'] = location return d def Plot(data,xaxis=None,yaxis=None,legend=None): d = {'data':data} - if xaxis != None: + if xaxis is not None: d['xaxis'] = xaxis - if yaxis != None: + if yaxis is not None: d['yaxis'] = yaxis - if legend != None: + if legend is not None: d['legend'] = legend def convertToText(data,title=None,xaxis=None,yaxis=None): output = '' - if title!=None: + if title is not None: output += title + '\n' - if xaxis != None: + if xaxis is not None: output += '# X' if 'label' in xaxis: output += ': ' + xaxis['label'] @@ -88,7 +87,7 @@ def convertToText(data,title=None,xaxis=None,yaxis=None): output += ': ' + str(xaxis['min']) + ' to ' + str(xaxis['max']) output+='\n' - if yaxis != None: + if yaxis is not None: output += '# Y' if 'label' in yaxis: output += ': ' + yaxis['label'] @@ -132,27 +131,27 @@ def makeGracePlot(data,title=None,xaxis=None,yaxis=None,legend=None): xrange = [0,1] yrange = [0,1] - if xaxis != None: + if xaxis is not None: if 'min' in xaxis and 'max' in xaxis: xrange = [ xaxis['min'],xaxis['max']] - if yaxis != None: + if yaxis is not None: if 'min' in yaxis and 'max' in yaxis: yrange = [ yaxis['min'],yaxis['max']] output += '@ world ' + str(xrange[0])+', ' + str (yrange[0]) + ',' output += str(xrange[1])+', ' + str (yrange[1]) + '\n' - if title != None: + if title is not None: output += '@ title "'+ title + '"\n' output += '@ title size 1.500000\n' xlog = False ylog = False - if xaxis != None: + if xaxis is not None: if 'logarithmic' in xaxis: xlog = xaxis['logarithmic'] - if yaxis != None: - if 'logarithmic' in yaxis and yaxis['logarithmic'] != None: + if yaxis is not None: + if 'logarithmic' in yaxis and yaxis['logarithmic'] is not None: ylog = yaxis['logarithmic'] if xlog: @@ -165,7 +164,7 @@ def makeGracePlot(data,title=None,xaxis=None,yaxis=None,legend=None): else: output += '@ yaxes scale Normal\n' - if xaxis != None: + if xaxis is not None: if 'label' in xaxis: output += '@ xaxis label "' + xaxis['label'] +'"\n' output += '@ xaxis label char size 1.500000\n' @@ -178,7 +177,7 @@ def makeGracePlot(data,title=None,xaxis=None,yaxis=None,legend=None): output += '@ xaxis ticklabel char size 1.250000\n' output += '@ xaxis tick minor ticks 4\n' - if yaxis != None: + if yaxis is not None: if 'label' in yaxis: output += '@ yaxis label "' + yaxis['label'] +'"\n' output += '@ yaxis label char size 1.500000\n' @@ -191,7 +190,7 @@ def makeGracePlot(data,title=None,xaxis=None,yaxis=None,legend=None): output += '@ yaxis ticklabel char size 1.250000\n' output += '@ yaxis tick minor ticks 4\n' - if legend != None and legend != False: + if legend is not None and legend != False: output += '@ legend on\n' output += '@ legend loctype view\n' output += '@ legend 0.85, 0.8\n' @@ -226,19 +225,19 @@ def makeGracePlot(data,title=None,xaxis=None,yaxis=None,legend=None): except AttributeError: yerrors = None - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += '@type xy\n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i]) + '\n' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += '@type xydy\n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i].mean) + '\t' + str(q.y[i].error) + '\n' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += '@type xydx\n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i].mean) + '\t' + str(q.x[i].error) + '\n' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += '@type xydxdy\n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i].mean) + '\t' + str(q.x[i].error) + '\t' + str(q.x[i].error) + '\n' @@ -262,37 +261,37 @@ def convert_to_grace(desc): def makeGnuplotPlot(data,title=None,xaxis=None,yaxis=None,legend=None, outfile=None, terminal=None, fontsize=24): output = '# Gnuplot project file\n' - if outfile != None: + if outfile is not None: output += 'set output "' + outfile + '"\n' - if terminal==None: + if terminal is None: if outfile[-3:]=='eps': terminal='postscript color eps enhanced ' + str(fontsize) if outfile[-3:]=='pdf': terminal='pdf color enhanced' - if terminal != None: + if terminal is not None: output += 'set terminal ' + str(terminal) +'\n' - if xaxis != None: + if xaxis is not None: if 'min' in xaxis and 'max' in xaxis: xrange = [ xaxis['min'],xaxis['max']] output += 'set xrange [' + str(xrange[0])+': ' + str (xrange[1]) + ']\n' - if yaxis != None: + if yaxis is not None: if 'min' in yaxis and 'max' in yaxis: yrange = [ yaxis['min'],yaxis['max']] output += 'set yrange [' + str(yrange[0])+': ' + str (yrange[1]) + ']\n' - if title != None: + if title is not None: output += 'set title "'+ title + '"\n' xlog = False ylog = False - if xaxis != None: + if xaxis is not None: if 'logarithmic' in xaxis: xlog = xaxis['logarithmic'] output += 'set xlogscale \n' else: output += '# no xlogscale \n' - if yaxis != None: + if yaxis is not None: if 'logarithmic' in yaxis: ylog = yaxis['logarithmic'] output += 'set ylogscale\n' @@ -300,15 +299,15 @@ def makeGnuplotPlot(data,title=None,xaxis=None,yaxis=None,legend=None, outfile=N output += '# no ylogscale\n' - if xaxis != None: + if xaxis is not None: if 'label' in xaxis: output += 'set xlabel "' + xaxis['label'] +'"\n' - if yaxis != None: + if yaxis is not None: if 'label' in yaxis: output += 'set ylabel "' + yaxis['label'] +'"\n' - if legend != None and legend != False: + if legend is not None and legend != False: output += 'set key top right\n' num = 0 @@ -327,62 +326,62 @@ def makeGnuplotPlot(data,title=None,xaxis=None,yaxis=None,legend=None, outfile=N yerrors = None if 'line' in q.props and q.props['line'] == 'scatter': if 'label' in q.props: - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += ' "-" using 1:2 title "' + q.props['label'] + '",' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += ' "-" using 1:2:3 w yerrorbars title "' + q.props['label'] + '",' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += ' "-" using 1:2:3 w xerrorbars title "' + q.props['label'] + '",' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += ' "-" using 1:2:3:4 w xyerrorbars title "' + q.props['label'] + '",' else: - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += ' "-" using 1:2 notitle ,"' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += ' "-" using 1:2:3 w yerrorbars notitle ,' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += ' "-" using 1:2:3 w xerrorbars notitle ,' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += ' "-" using 1:2:3:4 w xyerrorbars notitle ,' else: if 'label' in q.props: - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += ' "-" using 1:2 title "' + q.props['label'] + '",' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += ' "-" using 1:2:3 w yerrorline title "' + q.props['label'] + '",' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += ' "-" using 1:2:3 w xerrorline title "' + q.props['label'] + '",' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += ' "-" using 1:2:3:4 w xyerrorline title "' + q.props['label'] + '",' else: - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += ' "-" using 1:2 notitle ,"' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += ' "-" using 1:2:3 w yerrorline notitle ,' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += ' "-" using 1:2:3 w xerrorline notitle ,' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += ' "-" using 1:2:3:4 w xyerrorline notitle ,' output=output[:-1] output+='\n' for q in flatten(data): - if xerrors == None and yerrors == None: + if xerrors is None and yerrors is None: output += '# X Y \n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i]) + '\n' output += 'end \n' - if xerrors == None and yerrors != None: + if xerrors is None and yerrors is not None: output += '# X Y DY \n' for i in range(len(q.x)): output += str(q.x[i]) + '\t' + str(q.y[i].mean) + '\t' + str(q.y[i].error) + '\n' output += 'end \n' - if xerrors != None and yerrors == None: + if xerrors is not None and yerrors is None: output += '# X Y DX \n' for i in range(len(q.x)): output += str(q.x[i].mean) + '\t' + str(q.y[i]) + '\t' + str(q.x[i].error) + '\n' output += 'end \n' - if xerrors != None and yerrors != None: + if xerrors is not None and yerrors is not None: output += '# X Y DXY \n' for i in range(len(q.x)): output += str(q.x[i].mean) + '\t' + str(q.y[i].mean) + '\t' + str(q.x[i].error) + '\t' + str(q.y[i].error) + '\n' diff --git a/bindings/python/pyalps/src/pyalps/tools.py b/bindings/python/pyalps/src/pyalps/tools.py index 84067d143..6d3a1c57b 100644 --- a/bindings/python/pyalps/src/pyalps/tools.py +++ b/bindings/python/pyalps/src/pyalps/tools.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -136,10 +135,10 @@ def runApplication(appname, parmfiles, T=None, Tmin=None, Tmax=None, writexml=Fa for parmfile in parmfiles: cmdline = [] - if MPI != None: + if MPI is not None: cmdline += [mpirun,'-np',str(MPI)] cmdline += [appname] - if MPI != None: + if MPI is not None: cmdline += ['--mpi'] if appname in ['sparsediag','fulldiag','dmrg']: cmdline += ['--Nmax','1'] @@ -240,13 +239,13 @@ def evaluateFulldiagVersusT(infiles, appname='fulldiag_evaluate', DELTA_T=None, This function returns a list of lists of DataSet objects, for the various properties evaluated for each of the input files. """ cmdline = [appname] - if DELTA_T != None: + if DELTA_T is not None: cmdline += ['--DELTA_T',str(DELTA_T)] - if T_MIN != None: + if T_MIN is not None: cmdline += ['--T_MIN',str(T_MIN)] - if T_MAX != None: + if T_MAX is not None: cmdline += ['--T_MAX',str(T_MAX)] - if H != None: + if H is not None: cmdline += ['--H',str(H)] cmdline += make_list(infiles) res = executeCommand(cmdline) @@ -274,13 +273,13 @@ def evaluateFulldiagVersusH(infiles, appname='fulldiag_evaluate', DELTA_H=None, This function returns a list of lists of DataSet objects, for the various properties evaluated for each of the input files. """ cmdline = [appname,'--versus', 'h'] - if DELTA_H != None: + if DELTA_H is not None: cmdline += ['--DELTA_H',str(DELTA_H)] - if H_MIN != None: + if H_MIN is not None: cmdline += ['--H_MIN',str(H_MIN)] - if H_MAX != None: + if H_MAX is not None: cmdline += ['--H_MAX',str(H_MAX)] - if T != None: + if T is not None: cmdline += ['--T',str(T)] cmdline += make_list(infiles) res = executeCommand(cmdline) @@ -401,7 +400,7 @@ def writeInputFiles(fname,parms, baseseed=None): n //= 2 bits -= 1 - if baseseed == None: + if baseseed is None: baseseed = generateSeed() count = 0 @@ -488,11 +487,11 @@ def getResultFiles(dirname='.',pattern=None,prefix=None,format=None): The function returns a list of filenames """ - if prefix!= None and pattern != None: + if prefix is not None and pattern is not None: raise Exception("Cannot define both prefix and pattern") - if prefix == None: prefix = '*' - if pattern == None: - if format == None: + if prefix is None: prefix = '*' + if pattern is None: + if format is None: pattern = prefix+'.task*.out.xml' res=recursiveGlob(dirname, pattern) if len(res)==0: @@ -542,7 +541,7 @@ def getMeasurements(outfiles_, observable=None, includeLog=False): return measurements; def checkSteadyState(sets=None, outfile=None, observable=None, confidenceInterval=0.6827, includeLog=False): - if sets != None: + if sets is not None: results = [] for iset in flatten(sets): iset.props['checkSteadyState'] = checkSteadyState(outfile=iset.props['filename'], observable=iset.props['observable'], confidenceInterval=confidenceInterval, includeLog=True); @@ -587,9 +586,9 @@ def sendmail(recipients, sender=None, message='', subject='', attachment=None): subject = 'Automatic email message from ALPS. ' + str(subject); command = ['echo', message, '|', 'mail', '-s', subject]; - if sender != None: + if sender is not None: command += ['-r', sender]; - if attachment != None: + if attachment is not None: command += ['-a', attachment]; command += [recipients]; @@ -972,7 +971,7 @@ def saveMeasurements(measurements,outfile,respath='/simulation/results'): elif isinstance(m.y,np.ndarray) and isinstance(m.y[0],alea.MCScalarData): m.y[0].save(outfile,path) elif isinstance(m.y,FloatWithError): - h5f = h5.archive(fn, 'w') + h5f = h5.archive(outfile, 'w') h5f[path+'/mean/value'] = np.array(m.y.mean) h5f[path+'/mean/error'] = np.array(m.y.error) try: diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake index abf767c65..caa5f9ea6 100644 --- a/cmake/UsePyALPS.cmake +++ b/cmake/UsePyALPS.cmake @@ -3,8 +3,10 @@ # # Link a downstream nanobind module to the same ALPS runtime as pyalps. # -# Binary wheels relocate libalps and its non-system dependencies into a -# wheel-private directory. Linking a consumer module to a separately installed +# Binary wheels keep libalps and its non-system dependencies in +# wheel-private directories: the ALPS libraries in pyalps/lib, and the +# dependencies the repair tools relocated in pyalps.libs (auditwheel) or +# pyalps/.dylibs (delocate). Linking a consumer module to a separately installed # ALPS/HDF5 stack is unsafe: objects such as hdf5::archive carry handles that # are valid only in the HDF5 image that created them. This helper discovers a # repaired wheel's private runtime and links the target to those exact files. @@ -51,15 +53,38 @@ function(alps_target_link_pyalps target) ERROR_QUIET) if(_pyalps_location_result EQUAL 0 AND _pyalps_package_dir) - set(_pyalps_runtime_candidates - "${_pyalps_package_dir}/.dylibs" - "${_pyalps_package_dir}/../pyalps.libs") - foreach(_candidate IN LISTS _pyalps_runtime_candidates) + # A repair-tool directory is what marks this install as a relocated wheel: + # auditwheel writes /pyalps.libs, delocate writes + # pyalps/.dylibs. + set(_pyalps_repaired_dirs "") + foreach(_candidate IN ITEMS + "${_pyalps_package_dir}/.dylibs" + "${_pyalps_package_dir}/../pyalps.libs") if(IS_DIRECTORY "${_candidate}") - get_filename_component(_pyalps_private_runtime "${_candidate}" REALPATH) - break() + get_filename_component(_candidate "${_candidate}" REALPATH) + list(APPEND _pyalps_repaired_dirs "${_candidate}") endif() endforeach() + + if(_pyalps_repaired_dirs) + # The repaired runtime spans two directories. The ALPS libraries + # themselves are bundled into pyalps/lib by the wheel build -- one copy, + # shared with the programs in pyalps/bin -- while the repair tools + # vendor the external dependencies (HDF5, LAPACK, ...) into their own + # directory. Search both. + # + # pyalps/lib alone must not trigger this path: a developer install has + # that directory too, without any of the vendored dependencies the loop + # below insists on finding, and such an install is meant to keep using + # the ordinary ALPSConfig.cmake library paths. + if(IS_DIRECTORY "${_pyalps_package_dir}/lib") + get_filename_component(_pyalps_bundled_libs + "${_pyalps_package_dir}/lib" REALPATH) + list(APPEND _pyalps_private_runtime "${_pyalps_bundled_libs}") + endif() + list(APPEND _pyalps_private_runtime ${_pyalps_repaired_dirs}) + list(REMOVE_DUPLICATES _pyalps_private_runtime) + endif() endif() if(_pyalps_private_runtime) @@ -85,12 +110,16 @@ function(alps_target_link_pyalps target) set(_library_stem "hdf5") endif() - file(GLOB _matches LIST_DIRECTORIES FALSE - "${_pyalps_private_runtime}/lib${_library_stem}.so*" - "${_pyalps_private_runtime}/lib${_library_stem}-*.so*" - "${_pyalps_private_runtime}/lib${_library_stem}.dylib" - "${_pyalps_private_runtime}/lib${_library_stem}.*.dylib" - "${_pyalps_private_runtime}/lib${_library_stem}-*.dylib") + set(_matches "") + foreach(_runtime_dir IN LISTS _pyalps_private_runtime) + file(GLOB _runtime_dir_matches LIST_DIRECTORIES FALSE + "${_runtime_dir}/lib${_library_stem}.so*" + "${_runtime_dir}/lib${_library_stem}-*.so*" + "${_runtime_dir}/lib${_library_stem}.dylib" + "${_runtime_dir}/lib${_library_stem}.*.dylib" + "${_runtime_dir}/lib${_library_stem}-*.dylib") + list(APPEND _matches ${_runtime_dir_matches}) + endforeach() list(REMOVE_DUPLICATES _matches) list(LENGTH _matches _match_count) if(NOT _match_count EQUAL 1) @@ -127,17 +156,19 @@ function(alps_target_link_pyalps target) list(GET _install_name_lines 1 _install_name) string(STRIP "${_install_name}" _install_name) get_filename_component(_runtime_name "${_match}" NAME) - add_custom_command(TARGET "${target}" POST_BUILD - COMMAND "${CMAKE_INSTALL_NAME_TOOL}" -change - "${_install_name}" "@rpath/${_runtime_name}" - "$" - VERBATIM) + if(NOT _install_name STREQUAL "@rpath/${_runtime_name}") + add_custom_command(TARGET "${target}" POST_BUILD + COMMAND "${CMAKE_INSTALL_NAME_TOOL}" -change + "${_install_name}" "@rpath/${_runtime_name}" + "$" + VERBATIM) + endif() endif() endforeach() list(REMOVE_DUPLICATES _pyalps_private_libraries) set(_pyalps_link_libraries ${_pyalps_private_libraries}) - set(_pyalps_runtime_paths "${_pyalps_private_runtime}") + set(_pyalps_runtime_paths ${_pyalps_private_runtime}) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # GNU DT_RUNPATH is searched only for direct dependencies. Wheel # libraries such as LAPACK can themselves depend on relocated runtime @@ -145,8 +176,9 @@ function(alps_target_link_pyalps target) # transitive DT_RPATH tag for this consumer module instead. target_link_options("${target}" PRIVATE "LINKER:--disable-new-dtags") endif() + string(REPLACE ";" ", " _pyalps_runtime_report "${_pyalps_private_runtime}") message(STATUS - "${target}: using pyalps wheel runtime at ${_pyalps_private_runtime}") + "${target}: using pyalps wheel runtime at ${_pyalps_runtime_report}") else() target_link_directories("${target}" PRIVATE ${_pyalps_runtime_paths}) endif() diff --git a/cmake/passthru.py b/cmake/passthru.py index 00a7833cd..22228abe4 100755 --- a/cmake/passthru.py +++ b/cmake/passthru.py @@ -17,7 +17,7 @@ from subprocess import Popen, PIPE def verbose(what): - print what + print(what) # ignored # log = os.path.join(sys.argv[1], "Log.xml") @@ -35,18 +35,18 @@ def verbose(what): stdout = None stderr = None try: - print argv + print(argv) subproc = Popen(argv, stdout=PIPE, stderr=PIPE) (stdout, stderr) = subproc.communicate() -except EnvironmentError, e: +except OSError as e: ex = e returncode = subproc.returncode if stdout: - print stdout + print(stdout) if stderr: - print stderr + print(stderr) if not ex: # possibly flip the return code @@ -66,7 +66,7 @@ def verbose(what): else: # if there is an os error 'above' the actual exit status of the subprocess, # use the errno - print "Error in build system: " + str(ex.strerror) + print("Error in build system: " + str(ex.strerror)) sys.exit(ex.errno) diff --git a/example/alea/example_autocorrelation.py b/example/alea/example_autocorrelation.py index 67602b2ed..cc0ce0c16 100644 --- a/example/alea/example_autocorrelation.py +++ b/example/alea/example_autocorrelation.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/example/alea/example_error.py b/example/alea/example_error.py index 10c3ab005..c8cd0e16e 100644 --- a/example/alea/example_error.py +++ b/example/alea/example_error.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/example/alea/example_mean.py b/example/alea/example_mean.py index ae2514455..e997c6bd9 100644 --- a/example/alea/example_mean.py +++ b/example/alea/example_mean.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/example/alea/example_running_mean.py b/example/alea/example_running_mean.py index ac177e455..3392fed72 100644 --- a/example/alea/example_running_mean.py +++ b/example/alea/example_running_mean.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/example/alea/example_variance.py b/example/alea/example_variance.py index 11f9826eb..00ffa7fdc 100644 --- a/example/alea/example_variance.py +++ b/example/alea/example_variance.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/src/alps/hdf5/archive.cpp b/src/alps/hdf5/archive.cpp index ef9ce1e01..9340397ac 100644 --- a/src/alps/hdf5/archive.cpp +++ b/src/alps/hdf5/archive.cpp @@ -724,6 +724,14 @@ namespace alps { if (context_ == NULL) throw archive_closed("the archive is closed" + ALPS_STACKTRACE); ALPS_HDF5_FAKE_THREADSAFETY + // Resolve against the current context first, as every sibling here + // does. Without it a relative path -- in particular the empty path + // that paramvalue's saver uses, `ar[""] << value` under a context + // set to the parameter name -- produced the marker attribute path + // "/@__complex__", i.e. an attribute on the root group rather than + // on the dataset just written. Saving a complex parameter then + // failed with "HDF5 error: -1". + path = complete_path(path); if (path.find_last_of('@') != std::string::npos) write(path.substr(0, path.find_last_of('@')) + "@__complex__:" + path.substr(path.find_last_of('@') + 1), true); else { diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index 22373a0c5..27776bbf3 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -81,7 +81,15 @@ namespace alps { #define ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK(T, U) \ else if (ar.is_datatype< T >("")) \ ALPS_NGS_PARAMVALUE_LOAD_HDF5(U) - if (ar.is_scalar("")) { + // A complex scalar is stored as a trailing dimension of two + // reals, so archive::is_scalar() reports false for it and it fell + // into the vector branch below, where loading it as + // vector failed with "dimensions do not match". Rank + // tells them apart: a complex scalar has dimensions() == 1, a + // complex vector -- even a one-element one -- has 2. + if (ar.is_complex("") && ar.dimensions("") < 2) + ALPS_NGS_PARAMVALUE_LOAD_HDF5(std::complex) + else if (ar.is_scalar("")) { if (ar.is_complex("")) ALPS_NGS_PARAMVALUE_LOAD_HDF5(std::complex) ALPS_NGS_PARAMVALUE_LOAD_HDF5_CHECK(double, double) diff --git a/test/pyalps/accumulators.py b/test/pyalps/accumulators.py index 5ee8597ed..744aa222d 100644 --- a/test/pyalps/accumulators.py +++ b/test/pyalps/accumulators.py @@ -1,4 +1,3 @@ -from __future__ import print_function import numpy as np from pyngsaccumulator_c import count_accumulator, mean_accumulator, error_accumulator, binning_analysis_accumulator, max_num_binning_accumulator diff --git a/test/pyalps/loadobs.py b/test/pyalps/loadobs.py index 761624248..fd5611981 100644 --- a/test/pyalps/loadobs.py +++ b/test/pyalps/loadobs.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/test/pyalps/mcanalyze.py b/test/pyalps/mcanalyze.py deleted file mode 100644 index cf2ddd3be..000000000 --- a/test/pyalps/mcanalyze.py +++ /dev/null @@ -1,110 +0,0 @@ -from __future__ import print_function -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2010 by Lukas Gamper -# Matthias Troyer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - - - -import numpy -import pyalps -import mcanalyze -#import matplotlib.pyplot as plt -#import pyalps.plot as alpsplt -import pyalps.hdf5 as h5 - - - -print() -print("********** MCANALYZE PYTHON TEST **************") -print() - -rng = pyalps.pytools.rng(42) - -tmp1 = 1 -tmp2 = 1 -DATA = [] - -tmp1 = 1 -for i in range(10000): - tmp1 = 0.9 * tmp1 + 0.1 * (2*rng()-1) - DATA = DATA + [tmp1] - -NPDATA = numpy.array(DATA) - -#TS = mcanalyze.MCScalarTimeseries(NPDATA) -#Auto_corr = mcanalyze.autocorrelation_range(TS, mcanalyze.size(TS)-1) - -#binning_error = mcanalyze.binning_error(TS) -#uncorrelated_error = mcanalyze.uncorrelated_error(TS) - -#expo_corr_time = mcanalyze.exponential_autocorrelation_time_decay(Auto_corr, 1, 0.2) -#int_corr_time = mcanalyze.integrated_autocorrelation_time_decay(Auto_corr, expo_corr_time, 0.8) - -#print "Size: " + str(mcanalyze.size(TS)) -#print "Mean: " + str(mcanalyze.mean(TS)) -#print "Variance: " + str(mcanalyze.variance(TS)) -#print "integrated_corr_time: " + str(int_corr_time) -#print "binning_error: " + str(binning_error) -#print "uncorrelated_error: " + str(uncorrelated_error) - - - - -#auto = mcanalyze.autocorrelation_decay(TS,0.001) -#t = numpy.arange(0.0, mcanalyze.size(auto), 0.5) -#fit = mcanalyze.exponential_autocorrelation_time_range(auto, 1, mcanalyze.size(auto)/3) -#plt.plot(t, fit.first * numpy.exp(fit.second * t)) -#alpsplt.plot(mcanalyze.make_dataset(auto)) -#plt.show() - - - - - -print() -print("before") -print() - -#iar = h5.archive('test/scalartestfile.h5', 'r') -#for name in iar.list_children('/simulation/results'): -# if iar.is_scalar('/simulation/results/' + pyalps.hdf5_name_encode(name) + '/mean/value'): -# obs = pyalps.alea.MCScalarData() -# else: -# obs = pyalps.alea.MCVectorData() -# obs.load('test/scalartestfile.h5', '/simulation/results/' + pyalps.hdf5_name_encode(name)) -# print name + ": " + str(obs) - -#del iar - -#print mcanalyze.mean(obs) - -#mcanalyze.write_to_file('test/scalartestfile.h5', "E", "/mean/error", binning_error) - -print() -print("after") -print() - - -#iar = h5.archive('test/scalartestfile.h5', 'r') -#for name in iar.list_children('/simulation/results'): -# if iar.is_scalar('/simulation/results/' + pyalps.hdf5_name_encode(name) + '/mean/value'): -# obs = pyalps.alea.MCScalarData() -# else: -# obs = pyalps.alea.MCVectorData() -# obs.load('test/scalartestfile.h5', '/simulation/results/' + pyalps.hdf5_name_encode(name)) -# print name + ": " + str(obs) - - -print() -print("********** TEST END **************") -print() diff --git a/test/pyalps/numpylarge.py b/test/pyalps/numpylarge.py index 77cf29c18..251750590 100644 --- a/test/pyalps/numpylarge.py +++ b/test/pyalps/numpylarge.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/test/pyalps/pyhdf5_test.py b/test/pyalps/pyhdf5_test.py index 1f43f0736..692e60837 100644 --- a/test/pyalps/pyhdf5_test.py +++ b/test/pyalps/pyhdf5_test.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations @@ -75,3 +74,45 @@ def test_hdf5(): del ar print("SUCCESS") + + +def test_archive_open_state(): + """is_open is a property, and `closed` therefore actually works. + + The legacy Boost.Python module bound the name twice -- add_property + followed by .def -- and add_to_namespace only merges with an existing + *function*, so the .def overwrote the property and `ar.is_open` was a bound + method: always truthy, which left pyalps.hdf5.archive.closed permanently + False. The nanobind port binds is_open only as a read-only property. + """ + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "open_state.h5") + + ar = h5.archive(path, 'w') + assert ar.is_open is True + assert ar.closed is False + # regression guard: a property, not a callable + try: + ar.is_open() + except TypeError: + pass + else: + raise AssertionError("is_open must be a property, not a method") + + ar['/a'] = 1 + ar.close() + assert ar.is_open is False + assert ar.closed is True + + # xml() consults `closed`, so it must now refuse a closed archive + try: + ar.xml() + except h5.ArchiveClosed: + pass + else: + raise AssertionError("xml() on a closed archive must raise ArchiveClosed") + + # the context manager closes on exit + with h5.archive(path, 'r') as reader: + assert reader.is_open is True and reader.closed is False + assert reader.closed is True diff --git a/test/pyalps/pyioarchive.py b/test/pyalps/pyioarchive.py deleted file mode 100644 index 81e4d8c82..000000000 --- a/test/pyalps/pyioarchive.py +++ /dev/null @@ -1,28 +0,0 @@ - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - # # - # ALPS Project: Algorithms and Libraries for Physics Simulations # - # # - # ALPS Libraries # - # # - # Copyright (C) 2010 - 2012 by Lukas Gamper # - # # - # ALPS Project: https://alps.comp-phys.org/ # - # SPDX-License-Identifier: MIT # - # # - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - -import pyalps.hdf5 as h5 - -o = h5.archive('blubb', 'w') -o['/a'] = 0 -del o - -i = h5.archive('blubb', 'r') -o = h5.archive('blubb', 'w') -o['/a'] = 0 -del o - -del i -o = h5.archive('blubb', 'w') -o['/a'] = 0 -del o \ No newline at end of file diff --git a/test/pyalps/pyioarchive_test.py b/test/pyalps/pyioarchive_test.py new file mode 100644 index 000000000..0c955de5c --- /dev/null +++ b/test/pyalps/pyioarchive_test.py @@ -0,0 +1,53 @@ + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # + # ALPS Project: Algorithms and Libraries for Physics Simulations # + # # + # ALPS Libraries # + # # + # Copyright (C) 2010 - 2012 by Lukas Gamper # + # # + # ALPS Project: https://alps.comp-phys.org/ # + # SPDX-License-Identifier: MIT # + # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# Archive handle lifecycle: alps::hdf5::archive reference-counts one +# archivecontext per (filename, flags), so re-opening a file that already has a +# live handle shares that context. This exercises the overlap sequence -- open +# for write, keep a reader alive across two further write handles, then drop the +# reader last -- which is the only coverage in the suite for that path. +# +# Renamed from pyioarchive.py so pytest collects it; under the retired ctest +# runner the file name did not have to match a collection pattern. + +import os +import tempfile + +import pyalps.hdf5 as h5 + + +def test_overlapping_archive_handles(): + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "blubb") + + o = h5.archive(path, 'w') + o['/a'] = 0 + del o + + i = h5.archive(path, 'r') + o = h5.archive(path, 'w') + o['/a'] = 0 + del o + + del i + o = h5.archive(path, 'w') + o['/a'] = 0 + del o + + with h5.archive(path, 'r') as ar: + assert ar['/a'] == 0 + + +if __name__ == '__main__': + test_overlapping_archive_handles() + print("SUCCESS") diff --git a/test/pyalps/pyparams_test.py b/test/pyalps/pyparams_test.py index 7436c7660..bcb2d4027 100644 --- a/test/pyalps/pyparams_test.py +++ b/test/pyalps/pyparams_test.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 021c69342..986437562 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -18,6 +18,8 @@ from types import SimpleNamespace import numpy as np + +from pyalps.hdf5 import archive as hdf5_archive import pytest @@ -223,7 +225,7 @@ def test_accumulator_surface(): def test_optional_application_extension_surface(): - for name in ("maxent_c", "dwa_c", "cthyb", "ctint"): + for name in ("maxent_c", "cthyb", "ctint"): module = importlib.import_module("pyalps._ext." + name) assert module.__name__.endswith(name) assert importlib.import_module("pyalps." + name) is module @@ -233,16 +235,6 @@ def test_optional_application_extension_surface(): assert callable(cthyb.solve) assert callable(ctint.solve) - from pyalps._ext import dwa_c - - worldlines = dwa_c.worldlines(3) - assert worldlines.states() == [0, 0, 0] - assert dwa_c.std_vector_double([1.0, 2.0]) == [1.0, 2.0] - assert isinstance(worldlines.states(), dwa_c.std_vector_unsigned_short) - bands = dwa_c.bandstructure([1.0], [2.0], 1.0, 1.0, 1) - assert len(bands.t()) == 3 - - def test_ctqmc_solvers_restore_python_signal_handlers(tmp_path, monkeypatch): from pyalps import cthyb, ctint import pyalps.hdf5 as hdf5 @@ -309,6 +301,55 @@ def python_sigint_handler(signum, frame): assert len(calls) == 4 +def test_maxent_restores_python_signal_handlers(tmp_path, monkeypatch): + """MaxEnt must hand SIGINT back to Python, like cthyb and ctint do. + + Note the assertion style: signal.getsignal() is NOT a valid check here. + ALPS installs its handler with sigaction() behind CPython's back, so + getsignal() keeps reporting the Python handler while the OS-level + disposition belongs to ALPS -- an unguarded run passes a getsignal() + check and still swallows Ctrl-C, printing "Received signal 2" instead. + Only actually raising the signal and observing whether the Python + handler runs detects it. + """ + maxent = pytest.importorskip("pyalps.maxent_c") + + monkeypatch.chdir(tmp_path) + + ndat = 6 + parms = { + "BETA": 2.0, "NDAT": ndat, "NFREQ": 20, "N_ALPHA": 2, + "ALPHA_MIN": 0.1, "ALPHA_MAX": 1.0, "MAX_IT": 2, + "OMEGA_MAX": 4.0, "FREQUENCY_GRID": "linear", "KERNEL": "fermionic", + "DATASPACE": "time", "TEXT_OUTPUT": 0, "VERBOSE": 0, + "PARTICLE_HOLE_SYMMETRY": 1, "NORM": 1.0, "MAX_TIME": 1, + "BASENAME": str(tmp_path / "maxent-signal"), + } + for index in range(ndat): + parms["X_%d" % index] = -0.5 + parms["SIGMA_%d" % index] = 0.01 + + calls = [] + + def python_sigint_handler(signum, frame): + calls.append(signum) + + previous_handler = signal.signal(signal.SIGINT, python_sigint_handler) + try: + # Twice: restoring once is not enough if ALPS' own handlers are not + # reinstalled for the next embedded call. + for _ in range(2): + maxent.AnalyticContinuation(parms) + signal.raise_signal(signal.SIGINT) + finally: + signal.signal(signal.SIGINT, previous_handler) + + assert calls == [signal.SIGINT, signal.SIGINT], ( + "SIGINT was not handed back to Python after AnalyticContinuation; " + "ALPS still owns the OS-level handler" + ) + + def test_mpi4py_compatibility_surface(): pytest.importorskip("mpi4py") import operator @@ -568,9 +609,6 @@ def test_downstream_nanobind_simulation_export(tmp_path): def test_current_python_numpy_and_scipy_compatibility(monkeypatch): import pyalps - import pyalps.dwa as dwa - - assert callable(dwa.thermalized) parsed = pyalps.stringListToList("[1,[2,3],4]") assert parsed == [[1.0], [2.0, 3.0], [4.0]] @@ -712,6 +750,141 @@ def test_params_mapping_equality_and_value_ladder(): pass +def test_params_mapping_mixins_handle_none_getitem(): + """get/pop/setdefault must honour their contracts on params. + + params.__getitem__ returns None for an undefined key rather than raising + KeyError, so MutableMapping's mixins -- which are written against the + KeyError contract -- silently misbehaved: get() ignored its default, + setdefault() returned None and stored nothing, and pop() surfaced the C++ + "key does not exist" error instead of KeyError or the default. + """ + from pyalps import ngs + + p = ngs.params({"a": 1}) + + assert p["absent"] is None # the preserved legacy quirk + assert p.get("a") == 1 + assert p.get("absent") is None + assert p.get("absent", 9) == 9 + + assert p.setdefault("a", 5) == 1 and p["a"] == 1 + assert p.setdefault("new", 4) == 4 + assert "new" in p and p["new"] == 4 + + assert p.pop("new") == 4 and "new" not in p + assert p.pop("absent", 7) == 7 + with pytest.raises(KeyError): + p.pop("absent") + + # observables and results raise KeyError natively, so get() is fine there, + # but pop() needs the same replacement: MutableMapping.pop reads + # self._MutableMapping__marker, which a copied method cannot resolve. + obs = ngs.observables() + obs.createRealObservable("x") + assert obs.get("absent", 3) == 3 + assert obs.pop("absent", 3) == 3 + with pytest.raises(KeyError): + obs.pop("absent") + assert obs.pop("x") is not None and "x" not in obs + + +def test_archive_errors_use_the_typed_hierarchy(tmp_path): + """pyalps.hdf5's exception classes must actually be raised. + + nanobind compiles extensions with -fvisibility=hidden, so the catch + clauses in the exception translator could not match the exceptions libalps + threw: every archive failure arrived as a bare RuntimeError carrying the + whole ALPS_STACKTRACE, and ArchiveNotFound/ArchiveClosed were never seen. + Assert on the message length too -- a translated exception is trimmed to + its first line, so a multi-line message means the translator was bypassed. + """ + import pyalps.hdf5 as hdf5 + + with pytest.raises(hdf5.ArchiveNotFound) as missing: + hdf5.archive(str(tmp_path / "does-not-exist.h5"), "r") + assert len(str(missing.value).splitlines()) == 1 + + archive = hdf5.archive(str(tmp_path / "a.h5"), "w") + archive["/v"] = 1 + archive.close() + with pytest.raises(hdf5.ArchiveClosed) as closed: + archive["/v"] + assert len(str(closed.value).splitlines()) == 1 + + # every one of them derives from ArchiveError, so callers can catch broadly + for cls in (hdf5.ArchiveNotFound, hdf5.ArchiveClosed, hdf5.InvalidPath, + hdf5.PathNotFound, hdf5.WrongType): + assert issubclass(cls, hdf5.ArchiveError) + + +def test_complex_params_hdf5_roundtrip(tmp_path): + """Complex parameters must survive a checkpoint. + + Two separate defects made this fail. archive::set_complex() did not + resolve its path against the current context, so the marker attribute for + a value written at the empty path landed on the root group; and + paramvalue::load() sent complex scalars into the vector branch, because a + complex scalar has is_scalar() == false (it is stored as a trailing + dimension of two reals). Rank distinguishes them: 1 for a scalar, 2 for a + vector of any length. + """ + from pyalps import ngs + + cases = {"scalar": 1 + 2j, "vector": [1 + 2j, 3 + 4j], "one": [5 + 6j]} + for name, value in cases.items(): + path = str(tmp_path / ("complex-%s.h5" % name)) + with hdf5_archive(path, "w") as archive: + ngs.params({name: value}).save(archive) + loaded = ngs.params() + with hdf5_archive(path, "r") as archive: + loaded.load(archive, "/") + got = list(loaded[name]) if isinstance(value, list) else loaded[name] + assert got == value, "%s: %r != %r" % (name, got, value) + + +def test_mcbase_base_save_is_not_virtual(tmp_path): + """Calling the base save() from an override must not re-enter the override. + + save/load were bound as pointers-to-member, which dispatch through the + vtable, so ngs.mcbase.save(self, ar) -- and super().save(ar) -- landed back + in the Python override and ran its body twice. + """ + from pyalps import ngs + + class Base(ngs.mcbase): + def __init__(self, parms): + ngs.mcbase.__init__(self, parms, 42) + self.measurements.createRealObservable("E") + self.steps = 0 + + def update(self): + self.steps += 1 + + def measure(self): + self.measurements["E"] << 1.0 + + def fraction_completed(self): + return self.steps / 5.0 + + for label, use_super in (("explicit", False), ("super", True)): + calls = [] + + class Override(Base): + def save(self, archive): + calls.append(label) + if use_super: + super().save(archive) + else: + ngs.mcbase.save(self, archive) + + simulation = Override({"SWEEPS": 5, "THERMALIZATION": 0, "SEED": 1}) + simulation.run(lambda: False) + with hdf5_archive(str(tmp_path / ("mcbase-%s.h5" % label)), "w") as archive: + simulation.save(archive) + assert calls == [label], "%s: save() ran %d times" % (label, len(calls)) + + def test_params_native_bool_vector_hdf5_roundtrip(tmp_path): from pyalps import hdf5, ngs @@ -735,6 +908,33 @@ def test_observable_lshift_chains(): assert ngs.observable2result(observable).count == 2 +def test_standalone_observables_accept_samples(): + """ngs.createRealObservable() handles must accept measurements. + + They stopped doing so under nanobind: extensions are compiled with + -fvisibility=hidden, so instantiating a libalps class template inside a + binding TU emits a hidden vtable/type_info that cannot merge with + libalps' copy, and the dynamic_cast*> in + Observable::add then fails with "Cannot add measurement to observable". + The fix keeps construction on the libalps side; this pins it. + """ + import numpy as np + + from pyalps import ngs + + scalar = ngs.createRealObservable("Energy") + scalar << 1.0 + scalar << 2.0 + + vector = ngs.createRealVectorObservable("Correlations") + vector << np.array([1.0, 2.0, 3.0]) + + # the container-held equivalents must keep working too + observables = ngs.observables() + observables.createRealObservable("Energy") + observables["Energy"] << 1.5 + + def test_observables_item_deletion(): from pyalps import ngs @@ -747,6 +947,54 @@ def test_observables_item_deletion(): assert len(observables) == 0 +def test_mapping_views_are_set_like(): + """keys/values/items must be MutableMapping views, not one-shot iterators. + + Boost.Python's map_indexing_suite defined none of the three, so on the + legacy build they resolved through MutableMapping to KeysView/ValuesView/ + ItemsView: sized, re-iterable and set-like. The nanobind port must keep + that, which means NOT defining them natively in C++ -- pyalps/ngs.py only + grafts a mixin onto names the extension type leaves alone. + """ + from collections.abc import MutableMapping + + from pyalps import ngs + + observables = ngs.observables() + observables.createRealObservable("a") + observables.createRealObservable("b") + + for mapping in (observables, ngs.params({"a": 1, "b": 2})): + keys = mapping.keys() + # sized, and re-iterable (a nanobind iterator is exhausted after one pass) + assert len(keys) == 2 + assert sorted(keys) == ["a", "b"] + assert sorted(keys) == ["a", "b"] + # set-like + assert keys & {"a"} == {"a"} + assert keys | {"c"} == {"a", "b", "c"} + + items = mapping.items() + assert len(items) == 2 + assert sorted(k for k, _ in items) == ["a", "b"] + assert sorted(k for k, _ in items) == ["a", "b"] + + values = mapping.values() + assert len(values) == 2 + assert len(list(values)) == 2 + assert len(list(values)) == 2 + + # `results` is the third mapping type and goes through the same shim, but + # it is deliberately not constructible from Python -- master bound it with + # boost::python::no_init and the port binds no nb::init<> either -- so the + # view semantics are asserted here only through the two types that are. + for _name in ("keys", "values", "items"): + assert getattr(ngs.results, _name) is getattr(MutableMapping, _name), ( + "results.%s must come from the MutableMapping mixin, not a native " + "one-shot nanobind iterator" % _name + ) + + def test_mcbase_save_load_overrides_reach_cpp_dispatch(): from pyalps import ngs from pyalps.cxx import pyngshdf5_c @@ -777,16 +1025,22 @@ def load(self, archive): simulation.measurements["energy"] << 1.0 with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "checkpoint.h5") + # Drive a real C++-side checkpoint rather than calling the base + # binding: `archive[path] = simulation` hands the object to the C++ + # save path, which must reach the Python override. Calling + # ngs.mcbase.save(simulation, archive) would NOT test this -- that is + # the base implementation and deliberately does not dispatch + # virtually, so it cannot re-enter the override (see + # test_mcbase_base_save_is_not_virtual). archive = pyngshdf5_c.hdf5_archive_impl(path, "w") - # Call through the base binding: this goes through C++ virtual - # dispatch — the same path any C++-side checkpoint takes — and - # must reach the Python override (trampoline forwards save/load). - ngs.mcbase.save(simulation, archive) + archive["/simulation"] = simulation del archive assert calls == ["save"] - + # the override's super().save() must have written the real payload archive = pyngshdf5_c.hdf5_archive_impl(path, "r") - ngs.mcbase.load(simulation, archive) + assert "measurements" in archive.list_children("/simulation") + archive.set_context("/simulation") + simulation.load(archive) del archive assert calls == ["save", "load"] @@ -834,8 +1088,11 @@ def GetProperties(self, filenames): test_accumulator_surface, test_optional_application_extension_surface, test_params_mapping_equality_and_value_ladder, + test_params_mapping_mixins_handle_none_getitem, test_observable_lshift_chains, + test_standalone_observables_accept_samples, test_observables_item_deletion, + test_mapping_views_are_set_like, test_mcbase_save_load_overrides_reach_cpp_dispatch, test_accumulator_result_inplace_identity, ): diff --git a/test/pyalps/test_wheel_payload.py b/test/pyalps/test_wheel_payload.py new file mode 100644 index 000000000..72fc5d080 --- /dev/null +++ b/test/pyalps/test_wheel_payload.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +"""Guard what the wheel actually ships next to the extension modules. + +The wheel bundles the ALPS application programs (pyalps/bin) and the ALPS +shared libraries they link against (pyalps/lib), while auditwheel/delocate +vendor the genuinely external dependencies alongside them (pyalps.libs on +Linux, pyalps/.dylibs on macOS). Neither the ordinary binding tests nor the +wheel smoke tests ever load one of those programs, so two mistakes used to +travel undetected: shipping the same library more than once, and shipping +programs whose dynamic dependencies cannot be resolved from inside the +installed package. These tests run after `repair-wheel-command`, so they +check the repaired artifact rather than the build tree. +""" + +from __future__ import annotations + +import collections +from pathlib import Path +import re +import subprocess + +import pytest + +import pyalps + + +EXPECTED_BUNDLED_PROGRAMS = { + "checksign", + "dirloop_sse", + "dmft", + "dmrg", + "fulldiag", + "fulldiag_evaluate", + "hirschfye", + "hybridization", + "interaction", + "loop", + "qwl", + "qwl_evaluate", + "simplemc", + "sparsediag", + "spinmc", + "spinmc_evaluate", + "worm", + "worm_evaluate", +} + + +def _package_dir() -> Path: + return Path(pyalps.__file__).resolve().parent + + +def _library_dirs() -> list[Path]: + """Every directory in the installed package that holds bundled libraries.""" + pkg = _package_dir() + candidates = [pkg / "lib", pkg / ".dylibs", pkg.parent / f"{pkg.name}.libs"] + return [d for d in candidates if d.is_dir()] + + +def _library_stem(name: str) -> str: + """Reduce a shared-library file name to the library it is a copy of. + + ``libalps.so.2.3.4``, ``libalps-034f2e8c.so.2.3.4`` and ``libalps.dylib`` + all reduce to ``libalps``: the version suffixes and the content hash that + auditwheel/delocate append are what make duplicate copies look distinct. + """ + if ".so" in name: + stem = name.split(".so", 1)[0] + elif name.endswith(".dylib"): + stem = re.sub(r"(\.\d+)+$", "", name[: -len(".dylib")]) + else: + return "" + return re.sub(r"-[0-9a-f]{6,}$", "", stem) + + +def test_no_shared_library_is_bundled_twice(): + library_dirs = _library_dirs() + if not library_dirs: + pytest.skip("no bundled libraries in this install (source tree or SDK install)") + + seen: dict[str, list[Path]] = collections.defaultdict(list) + for directory in library_dirs: + for entry in sorted(directory.iterdir()): + if not entry.is_file(): + continue + stem = _library_stem(entry.name) + if stem: + seen[stem].append(entry) + + pkg_parent = _package_dir().parent + duplicates = { + stem: [str(p.relative_to(pkg_parent)) for p in paths] + for stem, paths in seen.items() + if len(paths) > 1 + } + assert not duplicates, ( + "the same shared library is bundled more than once; each copy is dead " + f"weight in the wheel: {duplicates}" + ) + + +def test_every_bundled_program_can_be_loaded(): + bin_dir = _package_dir() / "bin" + if not bin_dir.is_dir(): + pytest.skip("this install does not bundle the ALPS programs") + + programs = sorted(p for p in bin_dir.iterdir() if p.is_file()) + assert programs, f"{bin_dir} exists but is empty" + assert {p.name for p in programs} == EXPECTED_BUNDLED_PROGRAMS + + # Signatures the dynamic loader emits when a dependency cannot be resolved + # from inside the installed package. A program is free to reject --help + # however it likes -- several of these tools have no option parsing and + # abort on an uncaught C++ exception, so the exit status alone says nothing + # -- but it is not free to fail to start. dyld and glibc/musl both print a + # distinctive message before dying, which is what this matches on. + loader_errors = ( + "error while loading shared libraries", # glibc + "cannot open shared object file", # glibc, detail line + "Error loading shared library", # musl + "Library not loaded", # dyld + "image not found", # dyld + "ymbol not found", # dyld, either capitalisation + ) + + failures = [] + for program in programs: + try: + proc = subprocess.run( + [str(program), "--help"], + capture_output=True, + text=True, + timeout=60, + ) + except subprocess.TimeoutExpired: + # It started running, which is the only thing under test here. + continue + except OSError as exc: + failures.append(f"{program.name}: {exc}") + continue + output = f"{proc.stdout}\n{proc.stderr}" + hit = next((sig for sig in loader_errors if sig in output), None) + if hit is not None: + failures.append(f"{program.name}: loader error ({hit!r})") + elif proc.returncode == 127: + failures.append( + f"{program.name}: exited 127: {output.strip()[:200]}" + ) + + assert not failures, "bundled programs that cannot start:\n " + "\n ".join(failures) diff --git a/tool/alea/mcanalyze_tools.py b/tool/alea/mcanalyze_tools.py index c9eda22f8..c65062b03 100644 --- a/tool/alea/mcanalyze_tools.py +++ b/tool/alea/mcanalyze_tools.py @@ -1,4 +1,3 @@ -from __future__ import print_function #/***************************************************************************** #* #* ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tool/maxent.cpp b/tool/maxent.cpp index ce0a4aeac..c2225a563 100644 --- a/tool/maxent.cpp +++ b/tool/maxent.cpp @@ -46,9 +46,15 @@ bool stop_callback(boost::posix_time::ptime const & end_time) { #ifdef BUILD_PYTHON_MODULE #include "dict_to_params.hpp" +#include "scoped_signal_handlers.hpp" namespace nb = nanobind; void run_it(nb::dict const & parms_){ + // MaxEntSimulation derives from alps::mcbase, and stop_callback above + // constructs an alps::ngs::signal, so running it installs ALPS handlers + // for SIGINT and for SIGSEGV/SIGBUS. Restore Python's before returning + // to the interpreter, exactly as cthyb::solve and ctint::solve do. + pyalps::scoped_signal_handlers signal_handlers; alps::parameters_type::type parms = pyalps::params_from_dict(parms_); std::string out_file = boost::lexical_cast(parms["BASENAME"]|"results")+std::string(".out.h5"); diff --git a/tutorials/code-01-python/ising-skeleton.py b/tutorials/code-01-python/ising-skeleton.py index 0dcf43eaf..c84aa8f06 100644 --- a/tutorials/code-01-python/ising-skeleton.py +++ b/tutorials/code-01-python/ising-skeleton.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/experiment/experiment.py b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/experiment/experiment.py index cdba0b4d4..56e5de4e0 100644 --- a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/experiment/experiment.py +++ b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/experiment/experiment.py @@ -46,8 +46,8 @@ def run_exp(args): else: # good old sequential way of doing things for run_infile in runs: run_out = sp.check_output([args.program, run_infile]) - print '--- run {} ---'.format(run_infile) - print run_out + print('--- run {} ---'.format(run_infile)) + print(run_out) if not args.no_analysis: # run analysis if not otherwise specified analyze(args) @@ -108,12 +108,12 @@ def get_corr(result_files): cbins[i][j] = np.abs( np.sum(cbins[i][j]) / float(len(cbins[i][j])) ) # take the mean for every distance # group distance, mean correlation into a numpy array - result[i] = np.array([(d, c) for (d, c) in cbins[i].iteritems()], dtype = dist_corr_dt) + result[i] = np.array([(d, c) for (d, c) in cbins[i].items()], dtype = dist_corr_dt) result[i].sort(order='dist') # sort in order of ascending distance return result, T def analyze(args): - print 'running analysis' + print('running analysis') runs = pyalps.getResultFiles(prefix = args.infile) chi_data = get_chi(runs) diff --git a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/test/3d3d.py b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/test/3d3d.py index 3f675f653..20446370d 100755 --- a/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/test/3d3d.py +++ b/tutorials/code-07-mcmain-mcbase/heisenberg/o_n_model/test/3d3d.py @@ -20,7 +20,7 @@ sim.run(lambda: False) results = sim.collectResults() - print results + print(results) with pyalps.hdf5.archive(outfile, 'w') as ar: ar['parameters'] = sim.parameters diff --git a/tutorials/code-08-mcmain-python/ising.py b/tutorials/code-08-mcmain-python/ising.py index cf3ce6636..9e507c56d 100644 --- a/tutorials/code-08-mcmain-python/ising.py +++ b/tutorials/code-08-mcmain-python/ising.py @@ -13,7 +13,7 @@ import pyalps.ngs as ngs # move mcbase usw to pyalps.montecarlo import numpy as np import sys - +import traceback class sim: # TODO: how do we deal with typedefs? @@ -30,8 +30,8 @@ def __init__(self, params): self.length = int(self.parameters['L']) self.sweeps = 0 - self.thermalization_sweeps = long(self.parameters['THERMALIZATION']) - self.total_sweeps = long(self.parameters['SWEEPS']) + self.thermalization_sweeps = int(self.parameters['THERMALIZATION']) + self.total_sweeps = int(self.parameters['SWEEPS']) self.beta = 1. / float(self.parameters['T']) self.spins = np.array([(-x if self.random() < 0.5 else x) for x in np.ones(self.length)]) diff --git a/tutorials/code-08-mcmain-python/main.py b/tutorials/code-08-mcmain-python/main.py index dfcd987ed..bdb8b0cf2 100644 --- a/tutorials/code-08-mcmain-python/main.py +++ b/tutorials/code-08-mcmain-python/main.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ALPS Project: Algorithms and Libraries for Physics Simulations # # # @@ -53,7 +52,7 @@ ar['/'] = sim results = sim.collectResults() # TODO: how should we do that? - for key, value in results.iteritems(): + for key, value in results.items(): print("{}: {}".format(key, value)) with hdf5.archive(outfile, 'w') as ar: diff --git a/tutorials/code-09-mcmain-python-hybrid/ising.py b/tutorials/code-09-mcmain-python-hybrid/ising.py index 8b8a595a0..3f1923e56 100644 --- a/tutorials/code-09-mcmain-python-hybrid/ising.py +++ b/tutorials/code-09-mcmain-python-hybrid/ising.py @@ -25,8 +25,8 @@ def __init__(self, par, seed = 42): self.length = int(self.parameters['L']) self.sweeps = 0 - self.thermalization_sweeps = long(self.parameters['THERMALIZATION']) - self.total_sweeps = long(self.parameters['SWEEPS']) + self.thermalization_sweeps = int(self.parameters['THERMALIZATION']) + self.total_sweeps = int(self.parameters['SWEEPS']) self.beta = 1. / float(self.parameters['T']) self.spins = np.array([(-x if self.random() < 0.5 else x) for x in np.ones(self.length)]) diff --git a/tutorials/code-09-mcmain-python-hybrid/main.py b/tutorials/code-09-mcmain-python-hybrid/main.py index 37ffa3823..d84fdb000 100644 --- a/tutorials/code-09-mcmain-python-hybrid/main.py +++ b/tutorials/code-09-mcmain-python-hybrid/main.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ALPS Project: Algorithms and Libraries for Physics Simulations # # # @@ -57,4 +56,4 @@ results = ngs.collectResults(sim) print(results) with hdf5.archive(outfile, 'w') as ar: - ngs.saveResults(results, sim.paramters, ar, "/simulation/results") + ngs.saveResults(results, sim.parameters, ar, "/simulation/results") diff --git a/tutorials/dmft-02-hybridization/tutorial2eval.py b/tutorials/dmft-02-hybridization/tutorial2eval.py index c996a0efc..39a49ec43 100644 --- a/tutorials/dmft-02-hybridization/tutorial2eval.py +++ b/tutorials/dmft-02-hybridization/tutorial2eval.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/tutorials/dmft-03-interaction/tutorial3eval.py b/tutorials/dmft-03-interaction/tutorial3eval.py index c996a0efc..39a49ec43 100644 --- a/tutorials/dmft-03-interaction/tutorial3eval.py +++ b/tutorials/dmft-03-interaction/tutorial3eval.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/tutorials/dmft-07-hirschfye/tutorial7eval.py b/tutorials/dmft-07-hirschfye/tutorial7eval.py index c996a0efc..39a49ec43 100644 --- a/tutorials/dmft-07-hirschfye/tutorial7eval.py +++ b/tutorials/dmft-07-hirschfye/tutorial7eval.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/tutorials/dmft-08-lattices/DOS/DOS_Bethe.py b/tutorials/dmft-08-lattices/DOS/DOS_Bethe.py index 375b02b99..f70210269 100644 --- a/tutorials/dmft-08-lattices/DOS/DOS_Bethe.py +++ b/tutorials/dmft-08-lattices/DOS/DOS_Bethe.py @@ -17,12 +17,12 @@ from numpy import zeros from math import pi, sqrt -print "Description:" -print "This program produces file containing the density of states for the Bethe lattice (in infinitely many dimensions) to be read as input by ALPS application DMFT (with option DOSFILE)." -print "by Jakub Imriska\n" -print "Enter the discretization of the density of states (recommended divisible by 2, for Simpson integration later):" -BINS = eval(raw_input('--> ')) -print " (the output file will have ",BINS+1," rows, at the ends there are halves of bins)" +print("Description:") +print("This program produces file containing the density of states for the Bethe lattice (in infinitely many dimensions) to be read as input by ALPS application DMFT (with option DOSFILE).") +print("by Jakub Imriska\n") +print("Enter the discretization of the density of states (recommended divisible by 2, for Simpson integration later):") +BINS = eval(input('--> ')) +print(" (the output file will have ",BINS+1," rows, at the ends there are halves of bins)") # The DOS of Bethe lattice is semicircular, thus the normalized DOS is simply # DOS(e) = @@ -65,11 +65,11 @@ def Integrate(n): sum1 = 2. * sum1 + 4. * (sum2+func(BINS-1,n)) + func(0,n) + func(BINS,n); return sum1 * halfstep / 3. -print "Checks:" +print("Checks:") norm = Integrate(0) -print " normalization = ", norm," (close to 1)" -print " first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)" -print " second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 1.0)" +print(" normalization = ", norm," (close to 1)") +print(" first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)") +print(" second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 1.0)") plt.plot(energies[0:BINS+1],dos[0:BINS+1],'r-') plt.xlabel('energy / t --->') @@ -77,13 +77,13 @@ def Integrate(n): plt.title('DOS of the Bethe lattice') plt.show() -print "Do you wish to save the histogram [y/n] ?" -answer = raw_input('--> ') +print("Do you wish to save the histogram [y/n] ?") +answer = input('--> ') if answer[0]=='y': # write into file - print "Set the name for the histogram output file:" - file_name = raw_input('--> ') + print("Set the name for the histogram output file:") + file_name = input('--> ') file_out = open(file_name,'w') for j in range(0, BINS+1): file_out.write(str(energies[j])) diff --git a/tutorials/dmft-08-lattices/DOS/DOS_Cubic.py b/tutorials/dmft-08-lattices/DOS/DOS_Cubic.py index fed514ee5..8acf0b3f2 100644 --- a/tutorials/dmft-08-lattices/DOS/DOS_Cubic.py +++ b/tutorials/dmft-08-lattices/DOS/DOS_Cubic.py @@ -17,15 +17,15 @@ from numpy import zeros from math import sin, cos, pi, sqrt -print "Description:" -print "This program produces histogram of the density of states for cubic lattice in the tight-binding approximation, with nearest-neighbor hopping amplitude taken to be t=1." -print "by Jakub Imriska\n" -print "Enter the linear discretization GRID (cost: GRID^3/6):" -GRID = eval(raw_input('--> ')) -print " (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' x ', 2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)' -print "Enter the number of bins of the histogram (recommended divisible by 12, for Simpson integration later):" -BINS = eval(raw_input('--> ')) -print " (the output file will have ",BINS+1," rows, at the ends there are halves of bins)" +print("Description:") +print("This program produces histogram of the density of states for cubic lattice in the tight-binding approximation, with nearest-neighbor hopping amplitude taken to be t=1.") +print("by Jakub Imriska\n") +print("Enter the linear discretization GRID (cost: GRID^3/6):") +GRID = eval(input('--> ')) +print(" (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' x ', 2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)') +print("Enter the number of bins of the histogram (recommended divisible by 12, for Simpson integration later):") +BINS = eval(input('--> ')) +print(" (the output file will have ",BINS+1," rows, at the ends there are halves of bins)") # dispersion relation is given by: # e(kx,ky) = -2t (\cos(k_x*a) + \cos(k_y*a) + \cos(k_z*a)) @@ -93,7 +93,7 @@ def CompleteMultiplicity(x,y,z): for x in range(0,len(DOS)): counter+=DOS[x] DOS[x]*=inc -print "Number of processed k-points: ",counter, " (should be ", 8*GRID*GRID*GRID,')' +print("Number of processed k-points: ",counter, " (should be ", 8*GRID*GRID*GRID,')') # correct normalization for the 1st and last bin DOS[0] *= 2. # it is a half-bin (has only half of the usual width) @@ -127,13 +127,13 @@ def Integrate(n): sum1 = 2. * sum1 + 4. * (sum2+func(BINS-1,n)) + func(0,n) + func(BINS,n); return sum1 * halfstep / 3. -print "Checks:" +print("Checks:") norm = Integrate(0) -print " normalization = ", norm," (close to 1)" -print " first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)" -print " second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 6.0)" +print(" normalization = ", norm," (close to 1)") +print(" first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)") +print(" second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 6.0)") -print "Histogram created." +print("Histogram created.") plt.plot(energies[0:BINS+1],DOS[0:BINS+1],'r-') plt.xlabel('energy / t --->') @@ -141,13 +141,13 @@ def Integrate(n): plt.title('DOS of the cubic lattice') plt.show() -print "Do you wish to save the histogram [y/n] ?" -answer = raw_input('--> ') +print("Do you wish to save the histogram [y/n] ?") +answer = input('--> ') if answer[0]=='y': # write into file - print "Set the name for the histogram output file:" - file_name = raw_input('--> ') + print("Set the name for the histogram output file:") + file_name = input('--> ') file_out = open(file_name,'w') for j in range(0, BINS+1): file_out.write(str(energies[j])) diff --git a/tutorials/dmft-08-lattices/DOS/DOS_Hexagonal.py b/tutorials/dmft-08-lattices/DOS/DOS_Hexagonal.py index dbf76627d..0b533d83a 100644 --- a/tutorials/dmft-08-lattices/DOS/DOS_Hexagonal.py +++ b/tutorials/dmft-08-lattices/DOS/DOS_Hexagonal.py @@ -17,15 +17,15 @@ from numpy import zeros from math import sin, cos, pi, sqrt -print "Description:" -print "This program produces histogram of the density of states for hexagonal lattice in the tight-binding approximation, with hopping amplitude taken to be t=1." -print "by Jakub Imriska\n" -print "Enter the linear discretization GRID (cost: GRID^2):" -GRID = eval(raw_input('--> ')) -print " (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)' -print "Enter the number of bins of the histogram (recommended divisible by 12, for Simpson integration later):" -BINS = eval(raw_input('--> ')) -print " (the output file will have ",BINS+1," rows, at the ends there are halves of bins)" +print("Description:") +print("This program produces histogram of the density of states for hexagonal lattice in the tight-binding approximation, with hopping amplitude taken to be t=1.") +print("by Jakub Imriska\n") +print("Enter the linear discretization GRID (cost: GRID^2):") +GRID = eval(input('--> ')) +print(" (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)') +print("Enter the number of bins of the histogram (recommended divisible by 12, for Simpson integration later):") +BINS = eval(input('--> ')) +print(" (the output file will have ",BINS+1," rows, at the ends there are halves of bins)") # According to notes, the BZ may be chosen as: # k_x \in <0, 4\pi/3); k_y \in <0, 2\pi/\sqrt{3}) @@ -45,7 +45,7 @@ cos_x[i] = cos(pi*i/GRID) cos_y[i] = cos(pi*(i/(2.*GRID))) -print " ... histogram building in progres ..." +print(" ... histogram building in progres ...") # do histogram lower=0. # we look only on the plus branch and the minus branch will be added at the end @@ -80,7 +80,7 @@ def increment(kx,ky,m): for x in range(0,len(DOS)): counter+=DOS[x] DOS[x]*=inc -print "Number of processed k-points: ",counter, " (should be ", 4*GRID*GRID,')' +print("Number of processed k-points: ",counter, " (should be ", 4*GRID*GRID,')') # correct normalization for the 1st and last bin @@ -121,13 +121,13 @@ def Integrate(n): sum1 = 2. * sum1 + 4. * (sum2+func(BINS-1,n)) + func(0,n) + func(BINS,n); return sum1 * halfstep / 3. -print "Checks:" +print("Checks:") norm = Integrate(0) -print " normalization = ", norm," (close to 1)" -print " first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)" -print " second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 3.0)" +print(" normalization = ", norm," (close to 1)") +print(" first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)") +print(" second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 3.0)") -print "Histogram created." +print("Histogram created.") plt.plot(energies[0:BINS+1],dos[0:BINS+1],'r-') plt.xlabel('energy / t --->') @@ -135,13 +135,13 @@ def Integrate(n): plt.title('DOS of the hexagonal lattice') plt.show() -print "Do you wish to save the histogram [y/n] ?" -answer = raw_input('--> ') +print("Do you wish to save the histogram [y/n] ?") +answer = input('--> ') if answer[0]=='y': # write into file - print "Set the name for the histogram output file:" - file_name = raw_input('--> ') + print("Set the name for the histogram output file:") + file_name = input('--> ') file_out = open(file_name,'w') for j in range(0, BINS+1): file_out.write(str(energies[j])) diff --git a/tutorials/dmft-08-lattices/DOS/DOS_Square.py b/tutorials/dmft-08-lattices/DOS/DOS_Square.py index ba5f8c555..2e8e07d84 100644 --- a/tutorials/dmft-08-lattices/DOS/DOS_Square.py +++ b/tutorials/dmft-08-lattices/DOS/DOS_Square.py @@ -17,15 +17,15 @@ from numpy import zeros from math import sin, cos, pi, sqrt -print "Description:" -print "This program produces histogram of the density of states for square lattice in the tight-binding approximation, with nearest-neighbor hopping amplitude taken to be t=1." -print "by Jakub Imriska\n" -print "Enter the linear discretization GRID (cost: GRID^2/2):" -GRID = eval(raw_input('--> ')) -print " (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)' -print "Enter the number of bins of the histogram (recommended divisible by 4, for Simpson integration later):" -BINS = eval(raw_input('--> ')) -print " (the output file will have ",BINS+1," rows, at the ends there are halves of bins)" +print("Description:") +print("This program produces histogram of the density of states for square lattice in the tight-binding approximation, with nearest-neighbor hopping amplitude taken to be t=1.") +print("by Jakub Imriska\n") +print("Enter the linear discretization GRID (cost: GRID^2/2):") +GRID = eval(input('--> ')) +print(" (the histogram will be computed on a grid of ", 2*GRID,' x ',2*GRID,' k-points in the Brillouin zone; the program makes use of the symmetries)') +print("Enter the number of bins of the histogram (recommended divisible by 4, for Simpson integration later):") +BINS = eval(input('--> ')) +print(" (the output file will have ",BINS+1," rows, at the ends there are halves of bins)") # dispersion relation is given by: # e(kx,ky) = -2t (\cos(k_x*a) + \cos(k_y*a)) @@ -46,7 +46,7 @@ def increment(kx,ky,m): for i in range(-GRID,GRID): cos_[i+GRID] = cos(pi*i/GRID) -print " ... histogram building in progres ..." +print(" ... histogram building in progres ...") increment(GRID,GRID,1) increment(0,0,1) @@ -70,7 +70,7 @@ def increment(kx,ky,m): for x in range(0,len(DOS)): counter+=DOS[x] DOS[x]*=inc -print "Number of processed k-points: ",counter, " (should be ", 4*GRID*GRID,')' +print("Number of processed k-points: ",counter, " (should be ", 4*GRID*GRID,')') # correct normalization for the 1st and last bin DOS[0] *= 2. # it is a half-bin (has only half of the usual width) @@ -104,13 +104,13 @@ def Integrate(n): sum1 = 2. * sum1 + 4. * (sum2+func(BINS-1,n)) + func(0,n) + func(BINS,n); return sum1 * halfstep / 3. -print "Checks:" +print("Checks:") norm = Integrate(0) -print " normalization = ", norm," (close to 1)" -print " first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)" -print " second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 4.0)" +print(" normalization = ", norm," (close to 1)") +print(" first moment of the normalized DOS = ", Integrate(1)/norm," (exact: 0.0)") +print(" second moment of the normalized DOS = ", Integrate(2)/norm," (exact: 4.0)") -print "Histogram created." +print("Histogram created.") plt.plot(energies[0:BINS+1],DOS[0:BINS+1],'r-') plt.xlabel('energy / t --->') @@ -118,13 +118,13 @@ def Integrate(n): plt.title('DOS of the square lattice') plt.show() -print "Do you wish to save the histogram [y/n] ?" -answer = raw_input('--> ') +print("Do you wish to save the histogram [y/n] ?") +answer = input('--> ') if answer[0]=='y': # write into file - print "Set the name for the histogram output file:" - file_name = raw_input('--> ') + print("Set the name for the histogram output file:") + file_name = input('--> ') file_out = open(file_name,'w') for j in range(0, BINS+1): file_out.write(str(energies[j])) diff --git a/tutorials/dmrg-03-ground-state-energies/build_lattice.py b/tutorials/dmrg-03-ground-state-energies/build_lattice.py index a4ce8602e..0c045bc94 100755 --- a/tutorials/dmrg-03-ground-state-energies/build_lattice.py +++ b/tutorials/dmrg-03-ground-state-energies/build_lattice.py @@ -14,7 +14,6 @@ # # **************************************************************************** -from __future__ import print_function import sys ns = sys.argv[1] diff --git a/tutorials/dmrg-03-ground-state-energies/spin_one.py b/tutorials/dmrg-03-ground-state-energies/spin_one.py index c19381839..6d03435ba 100644 --- a/tutorials/dmrg-03-ground-state-energies/spin_one.py +++ b/tutorials/dmrg-03-ground-state-energies/spin_one.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-03-ground-state-energies/spin_one_half.py b/tutorials/dmrg-03-ground-state-energies/spin_one_half.py index 806df88fd..034079350 100644 --- a/tutorials/dmrg-03-ground-state-energies/spin_one_half.py +++ b/tutorials/dmrg-03-ground-state-energies/spin_one_half.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-03-ground-state-energies/spin_one_half_multiple.py b/tutorials/dmrg-03-ground-state-energies/spin_one_half_multiple.py index d676919e2..1b7018d29 100644 --- a/tutorials/dmrg-03-ground-state-energies/spin_one_half_multiple.py +++ b/tutorials/dmrg-03-ground-state-energies/spin_one_half_multiple.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-03-ground-state-energies/spin_one_multiple.py b/tutorials/dmrg-03-ground-state-energies/spin_one_multiple.py index 30ee390dc..1c2687922 100644 --- a/tutorials/dmrg-03-ground-state-energies/spin_one_multiple.py +++ b/tutorials/dmrg-03-ground-state-energies/spin_one_multiple.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-04-gaps/spin_one_half_gap.py b/tutorials/dmrg-04-gaps/spin_one_half_gap.py index 0a7131183..02ff91e05 100644 --- a/tutorials/dmrg-04-gaps/spin_one_half_gap.py +++ b/tutorials/dmrg-04-gaps/spin_one_half_gap.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-04-gaps/spin_one_half_triplet.py b/tutorials/dmrg-04-gaps/spin_one_half_triplet.py index 9623e62a0..02e8f7ddc 100644 --- a/tutorials/dmrg-04-gaps/spin_one_half_triplet.py +++ b/tutorials/dmrg-04-gaps/spin_one_half_triplet.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-05-local-observables/build_lattice.py b/tutorials/dmrg-05-local-observables/build_lattice.py index a4ce8602e..0c045bc94 100755 --- a/tutorials/dmrg-05-local-observables/build_lattice.py +++ b/tutorials/dmrg-05-local-observables/build_lattice.py @@ -14,7 +14,6 @@ # # **************************************************************************** -from __future__ import print_function import sys ns = sys.argv[1] diff --git a/tutorials/dmrg-06-correlations/spin_one.py b/tutorials/dmrg-06-correlations/spin_one.py index 09fc3284c..4efb1c14f 100644 --- a/tutorials/dmrg-06-correlations/spin_one.py +++ b/tutorials/dmrg-06-correlations/spin_one.py @@ -1,4 +1,3 @@ -from __future__ import division # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/dmrg-06-correlations/spin_one_half.py b/tutorials/dmrg-06-correlations/spin_one_half.py index 862ca3802..bba1abdde 100644 --- a/tutorials/dmrg-06-correlations/spin_one_half.py +++ b/tutorials/dmrg-06-correlations/spin_one_half.py @@ -1,4 +1,3 @@ -from __future__ import division # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/ed-01-sparsediag/tutorial1a.py b/tutorials/ed-01-sparsediag/tutorial1a.py index e34924f3c..a5df4d460 100644 --- a/tutorials/ed-01-sparsediag/tutorial1a.py +++ b/tutorials/ed-01-sparsediag/tutorial1a.py @@ -1,4 +1,3 @@ -from __future__ import print_function # # ALPS Project: Algorithms and Libraries for Physics Simulations # diff --git a/tutorials/ed-05-nnn-chain/nnn-crit-pt.py b/tutorials/ed-05-nnn-chain/nnn-crit-pt.py index 527e34edc..c4625c735 100644 --- a/tutorials/ed-05-nnn-chain/nnn-crit-pt.py +++ b/tutorials/ed-05-nnn-chain/nnn-crit-pt.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/hybridization-02-kondo/tutorial2.py b/tutorials/hybridization-02-kondo/tutorial2.py index c2cac627e..cf9cfed66 100644 --- a/tutorials/hybridization-02-kondo/tutorial2.py +++ b/tutorials/hybridization-02-kondo/tutorial2.py @@ -1,4 +1,3 @@ -from __future__ import print_function #############################################################################/ # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/hybridization-03-retarded-interaction/tutorial3.py b/tutorials/hybridization-03-retarded-interaction/tutorial3.py index 9c7c130ea..e5606ddc5 100644 --- a/tutorials/hybridization-03-retarded-interaction/tutorial3.py +++ b/tutorials/hybridization-03-retarded-interaction/tutorial3.py @@ -1,4 +1,3 @@ -from __future__ import print_function #############################################################################/ # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/hybridization-04-spinfreezing/tutorial4a.py b/tutorials/hybridization-04-spinfreezing/tutorial4a.py index 95d2f2b43..ce4c1fe51 100644 --- a/tutorials/hybridization-04-spinfreezing/tutorial4a.py +++ b/tutorials/hybridization-04-spinfreezing/tutorial4a.py @@ -1,4 +1,3 @@ -from __future__ import print_function #############################################################################/ # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/hybridization-04-spinfreezing/tutorial4b.py b/tutorials/hybridization-04-spinfreezing/tutorial4b.py index e62037e2b..43b094fa4 100644 --- a/tutorials/hybridization-04-spinfreezing/tutorial4b.py +++ b/tutorials/hybridization-04-spinfreezing/tutorial4b.py @@ -1,4 +1,3 @@ -from __future__ import print_function #############################################################################/ # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-binder.py b/tutorials/intro-01-basics/tutorial-binder.py index 66fb9ce3f..476b947ed 100644 --- a/tutorials/intro-01-basics/tutorial-binder.py +++ b/tutorials/intro-01-basics/tutorial-binder.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-evaluate.py b/tutorials/intro-01-basics/tutorial-evaluate.py index f7dfd31de..b676e4dc8 100644 --- a/tutorials/intro-01-basics/tutorial-evaluate.py +++ b/tutorials/intro-01-basics/tutorial-evaluate.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-full.py b/tutorials/intro-01-basics/tutorial-full.py index f95b43226..8767e2d4a 100644 --- a/tutorials/intro-01-basics/tutorial-full.py +++ b/tutorials/intro-01-basics/tutorial-full.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-gnuplot.py b/tutorials/intro-01-basics/tutorial-gnuplot.py index 9b4a605be..0965b0867 100644 --- a/tutorials/intro-01-basics/tutorial-gnuplot.py +++ b/tutorials/intro-01-basics/tutorial-gnuplot.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-graceplot.py b/tutorials/intro-01-basics/tutorial-graceplot.py index 56bcca1f4..d0f9c228f 100644 --- a/tutorials/intro-01-basics/tutorial-graceplot.py +++ b/tutorials/intro-01-basics/tutorial-graceplot.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-magnetization.py b/tutorials/intro-01-basics/tutorial-magnetization.py index f903844f6..e456f6a69 100644 --- a/tutorials/intro-01-basics/tutorial-magnetization.py +++ b/tutorials/intro-01-basics/tutorial-magnetization.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-prepareinput.py b/tutorials/intro-01-basics/tutorial-prepareinput.py index 6be0394a2..612ba2568 100644 --- a/tutorials/intro-01-basics/tutorial-prepareinput.py +++ b/tutorials/intro-01-basics/tutorial-prepareinput.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-runsimulation.py b/tutorials/intro-01-basics/tutorial-runsimulation.py index 2ff56ae6a..07f0bac27 100644 --- a/tutorials/intro-01-basics/tutorial-runsimulation.py +++ b/tutorials/intro-01-basics/tutorial-runsimulation.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/intro-01-basics/tutorial-text.py b/tutorials/intro-01-basics/tutorial-text.py index d1476af4a..43f20d794 100644 --- a/tutorials/intro-01-basics/tutorial-text.py +++ b/tutorials/intro-01-basics/tutorial-text.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/mc-01b-equilibration-and-convergence/tutorial1a.py b/tutorials/mc-01b-equilibration-and-convergence/tutorial1a.py index 576fda65c..ec5d2b808 100644 --- a/tutorials/mc-01b-equilibration-and-convergence/tutorial1a.py +++ b/tutorials/mc-01b-equilibration-and-convergence/tutorial1a.py @@ -1,4 +1,3 @@ -from __future__ import print_function ############################################################################# # # ALPS Project Applications: Directed Worm Algorithm diff --git a/tutorials/mc-04-measurements/tutorial4.py b/tutorials/mc-04-measurements/tutorial4.py index 61d4de55c..7e93f6149 100644 --- a/tutorials/mc-04-measurements/tutorial4.py +++ b/tutorials/mc-04-measurements/tutorial4.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/mc-06-qwl/tutorial6d.py b/tutorials/mc-06-qwl/tutorial6d.py index ad81d6cd7..eb37496f3 100644 --- a/tutorials/mc-06-qwl/tutorial6d.py +++ b/tutorials/mc-06-qwl/tutorial6d.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/mc-08-quantum-phase-transition/tutorial8a.py b/tutorials/mc-08-quantum-phase-transition/tutorial8a.py index 08c3bcf6f..91dd57906 100644 --- a/tutorials/mc-08-quantum-phase-transition/tutorial8a.py +++ b/tutorials/mc-08-quantum-phase-transition/tutorial8a.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations diff --git a/tutorials/ngs/6_python_native/ising.py b/tutorials/ngs/6_python_native/ising.py index 243fc86f7..ca1e56219 100644 --- a/tutorials/ngs/6_python_native/ising.py +++ b/tutorials/ngs/6_python_native/ising.py @@ -13,7 +13,7 @@ import pyalps.ngs as ngs # move mcbase usw to pyalps.montecarlo import numpy as np import sys - +import traceback class sim: # TODO: how do we deal with typedefs? @@ -30,8 +30,8 @@ def __init__(self, params): self.length = int(self.parameters['L']) self.sweeps = 0 - self.thermalization_sweeps = long(self.parameters['THERMALIZATION']) - self.total_sweeps = long(self.parameters['SWEEPS']) + self.thermalization_sweeps = int(self.parameters['THERMALIZATION']) + self.total_sweeps = int(self.parameters['SWEEPS']) self.beta = 1. / float(self.parameters['T']) self.spins = np.array([(-x if self.random() < 0.5 else x) for x in np.ones(self.length)]) diff --git a/tutorials/ngs/6_python_native/main.py b/tutorials/ngs/6_python_native/main.py index ff3585030..bdb8b0cf2 100644 --- a/tutorials/ngs/6_python_native/main.py +++ b/tutorials/ngs/6_python_native/main.py @@ -26,7 +26,7 @@ resume = True if 'c' in args else False outfile = positional[0] except (IndexError, getopt.GetoptError): - print 'usage: [-T timelimit] [-c] outputfile' + print('usage: [-T timelimit] [-c] outputfile') exit() sim = ising.sim({ @@ -52,8 +52,8 @@ ar['/'] = sim results = sim.collectResults() # TODO: how should we do that? - for key, value in results.iteritems(): - print "{}: {}".format(key, value) + for key, value in results.items(): + print("{}: {}".format(key, value)) with hdf5.archive(outfile, 'w') as ar: ar['/parameters'] = sim.parameters diff --git a/tutorials/ngs/7_python_extend/ising.py b/tutorials/ngs/7_python_extend/ising.py index 296bc6ca0..725ffd796 100644 --- a/tutorials/ngs/7_python_extend/ising.py +++ b/tutorials/ngs/7_python_extend/ising.py @@ -25,8 +25,8 @@ def __init__(self, par, seed = 42): self.length = int(self.parameters['L']) self.sweeps = 0 - self.thermalization_sweeps = long(self.parameters['THERMALIZATION']) - self.total_sweeps = long(self.parameters['SWEEPS']) + self.thermalization_sweeps = int(self.parameters['THERMALIZATION']) + self.total_sweeps = int(self.parameters['SWEEPS']) self.beta = 1. / float(self.parameters['T']) self.spins = np.array([(-x if self.random() < 0.5 else x) for x in np.ones(self.length)]) diff --git a/tutorials/ngs/7_python_extend/main.py b/tutorials/ngs/7_python_extend/main.py index fab878049..d84fdb000 100644 --- a/tutorials/ngs/7_python_extend/main.py +++ b/tutorials/ngs/7_python_extend/main.py @@ -28,7 +28,7 @@ resume = True if 'c' in args else False outfile = positional[0] except (IndexError, getopt.GetoptError): - print 'usage: [-T timelimit] [-c] outputfile' + print('usage: [-T timelimit] [-c] outputfile') exit() sim = ising.sim({ @@ -54,6 +54,6 @@ ar['/'] = sim results = ngs.collectResults(sim) - print results + print(results) with hdf5.archive(outfile, 'w') as ar: - ngs.saveResults(results, sim.paramters, ar, "/simulation/results") + ngs.saveResults(results, sim.parameters, ar, "/simulation/results") diff --git a/tutorials/notebook/ja/tutorial_ed01a.py b/tutorials/notebook/ja/tutorial_ed01a.py index c0e25b168..62ca31901 100644 --- a/tutorials/notebook/ja/tutorial_ed01a.py +++ b/tutorials/notebook/ja/tutorial_ed01a.py @@ -34,12 +34,12 @@ # print properties of ground states in all sectors: for sector in data[0]: - print '\nSector with Sz =', sector[0].props['Sz'], - print 'and k =', sector[0].props['TOTAL_MOMENTUM'] + print('\nSector with Sz =', sector[0].props['Sz'], end=' ') + print('and k =', sector[0].props['TOTAL_MOMENTUM']) for s in sector: if pyalps.size(s.y[0])==1: - print s.props['observable'], ' : ', s.y[0] + print(s.props['observable'], ' : ', s.y[0]) else: for (x,y) in zip(s.x,s.y[0]): - print s.props['observable'], '(', x, ') : ', y + print(s.props['observable'], '(', x, ') : ', y) diff --git a/tutorials/notebook/ja/tutorial_mc01b.py b/tutorials/notebook/ja/tutorial_mc01b.py index 1bb223a84..c3c80c333 100644 --- a/tutorials/notebook/ja/tutorial_mc01b.py +++ b/tutorials/notebook/ja/tutorial_mc01b.py @@ -45,12 +45,12 @@ # ALPS Python provides a convenient tool to check whether a measurement observable(s) has (have) reached steady state equilibrium. # # Here is one example: -print pyalps.checkSteadyState(outfile=files[0], observable='|Magnetization|', confidenceInterval=0.95) +print(pyalps.checkSteadyState(outfile=files[0], observable='|Magnetization|', confidenceInterval=0.95)) print # and another one: observables = pyalps.loadMeasurements(files, ['|Magnetization|', 'Energy']) observables = pyalps.checkSteadyState(observables, confidenceInterval=0.95) for o in observables: - print '{}:\t{}'.format(o.props['observable'], o.props['checkSteadyState']) + print('{}:\t{}'.format(o.props['observable'], o.props['checkSteadyState'])) diff --git a/tutorials/notebook/ja/tutorial_mc04.py b/tutorials/notebook/ja/tutorial_mc04.py index 5a0704f07..606aea9a2 100644 --- a/tutorials/notebook/ja/tutorial_mc04.py +++ b/tutorials/notebook/ja/tutorial_mc04.py @@ -39,7 +39,7 @@ # print all measurements for s in pyalps.flatten(data): if len(s.x)==1: - print s.props['observable'], ' : ', s.y[0] + print(s.props['observable'], ' : ', s.y[0]) else: for (x,y) in zip(s.x,s.y): - print s.props['observable'], x, ' : ', y + print(s.props['observable'], x, ' : ', y) diff --git a/tutorials/notebook/ja/tutorial_mc06d.py b/tutorials/notebook/ja/tutorial_mc06d.py index e15a8c0bc..ef9c4fd5f 100644 --- a/tutorials/notebook/ja/tutorial_mc06d.py +++ b/tutorials/notebook/ja/tutorial_mc06d.py @@ -42,7 +42,7 @@ data = [] for s in pyalps.flatten(results): if s.props['ylabel']=='Staggered Structure Factor per Site': - print 'yes' + print('yes') d = copy.deepcopy(s) # make a deep copy to not change the original l = s.props['L'] d.props['label']='L='+str(l) diff --git a/tutorials/notebook/ja/tutorial_mc08a.py b/tutorials/notebook/ja/tutorial_mc08a.py index 8063e1827..d4f8fb80f 100644 --- a/tutorials/notebook/ja/tutorial_mc08a.py +++ b/tutorials/notebook/ja/tutorial_mc08a.py @@ -56,7 +56,7 @@ fw.fit(None, f, pars, [v.mean for v in data.y], data.x) prefactor = pars[0].get() gap = pars[1].get() - print prefactor,gap + print(prefactor,gap) lines += plt.plot(data.x, f(None, data.x, pars)) lines[-1].set_label('$J_2=%.4s$: $\chi = \frac{%.4s}{T}\exp(\frac{-%.4s}{T})$' % (data.props['J2'], prefactor,gap)) diff --git a/tutorials/test_py.py b/tutorials/test_py.py index 0581fbc9a..b52d0d04d 100755 --- a/tutorials/test_py.py +++ b/tutorials/test_py.py @@ -1,4 +1,3 @@ -from __future__ import print_function # **************************************************************************** # # ALPS Project: Algorithms and Libraries for Physics Simulations From 9c35382338fa75fdcaac580bbe2a003f22f49df7 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 20 Aug 2026 23:47:02 -0500 Subject: [PATCH 33/52] fix(pyalps): close the archive save gap and revive the dead tests Six follow-ups from assessing the nanobind migration against master. Each was found by running something the migration had left unexecuted. archive[path] = object could not store any ALPS extension type. The gate in ngs/hdf5.cpp accepted only bound methods whose type name is "method" -- those defined in Python -- and its comment claimed registered types kept a "native save path" that does not exist, so params, observables, results and the rest fell through to extract_from_pyobject and were reported as unsupported even though obj.save(archive) worked when called directly. This is a Python-2-era capability: Boost.Python's bound methods really were CPython methods, which Python 2 named "instancemethod", so master's gate matched them and stopped matching anything the day pyalps moved to Python 3. Rather than enumerate the types in hdf5.cpp -- there are sixteen, ten of them accumulator template instantiations owned by another translation unit -- the capability is now declared where each class is bound, via pyalps::mark_archive_savable in cpp/archive_savable.hpp. The archive module needs no includes for and no knowledge of any of them, and a type added later works without touching it. MCScalarData/MCVectorData deliberately do not declare it, since their save() takes (filename, observable_name); they now raise a TypeError naming the spelling that does work. Container children were bypassing the dispatch entirely: the list and dict branches of the save visitor recursed straight into extract_from_pyobject, so archive[p] = {'Energy': observable} failed while archive[p] = observable worked. They now recurse through python_hdf5_save, which is what makes a checkpoint -- a mapping of observables -- writable at all. str() raised TypeError on all ten accumulator and result types: __str__ is bound as a std::string-returning function but ngs/accumulator.cpp never included nanobind/stl/string.h. Invisible for as long as the test that would have caught it was uncollectable by pytest. Also: - Delete src/alps/python/make_copy.hpp, an unused nanobind duplicate of the Python-free src/alps/utility/make_copy.hpp that was installed as a public header of an SDK documented to have no Python dependency. - Keep one save_observable_to_hdf5.hpp: pyalea.cpp had been including a stale bindings-local copy rather than the cleaned SDK one. - Stop baking the build machine's absolute paths into every wheel. pyalps_config.py's fallbacks are for builds that do not bundle their resources, so they are empty when the package does bundle them -- the default, and what every published wheel is. tools.py treats an absent fallback as absent instead of exporting an empty path. - Replace the three test files pytest never collected. accumulators.py used a flat import that only resolved under the old in-tree build layout; numpylarge.py wrote 4 GiB, asserted nothing and littered the source tree; loadobs.py and loadobs.cpp both read a fixture neither created and that ships nowhere. The replacements assert, run in tmp_path, and build their own fixtures. loadobs_test.py also pins down the encoding rule both originals got wrong: list_children returns names as stored, already encoded, and re-encoding them double-encodes anything containing '/' or '&'. - Give tutorials/ngs/{6_python_native,7_python_extend} smoke tests, run from the pytest suite so cibuildwheel exercises them against every wheel. Both needed repairs to checkpoint code that had never executed: save/load defined twice in tutorial 6 with the first pair shadowed, a load() that wrote to the archive instead of reading from it, double() in both, tutorial 7 reading its checkpoint from paths save() does not write, and a bare ArchiveNotFound in both main.py files that would have raised NameError. Suite goes from 43 passed to 80. Numeric fidelity re-verified after the changes: master's own test scripts still reproduce master's reference output byte for byte. Co-Authored-By: Claude Opus 5 --- bindings/python/pyalps/CMakeLists.txt | 18 ++- .../python/pyalps/cpp/archive_savable.hpp | 36 +++++ .../python/pyalps/cpp/ngs/accumulator.cpp | 7 + bindings/python/pyalps/cpp/ngs/hdf5.cpp | 96 ++++++++---- bindings/python/pyalps/cpp/ngs/mcbase.cpp | 2 + bindings/python/pyalps/cpp/ngs/observable.cpp | 2 + .../python/pyalps/cpp/ngs/observables.cpp | 2 + bindings/python/pyalps/cpp/ngs/params.cpp | 2 + bindings/python/pyalps/cpp/ngs/random01.cpp | 2 + bindings/python/pyalps/cpp/ngs/result.cpp | 2 + bindings/python/pyalps/cpp/ngs/results.cpp | 2 + bindings/python/pyalps/cpp/pyalea.cpp | 2 +- .../pyalps/cpp/save_observable_to_hdf5.hpp | 15 -- .../pyalps/src/pyalps/pyalps_config.py.in | 15 +- bindings/python/pyalps/src/pyalps/tools.py | 44 +++--- src/alps/python/make_copy.hpp | 20 --- test/pyalps/accumulators.py | 70 --------- test/pyalps/accumulators_test.py | 141 +++++++++++++++++ test/pyalps/loadobs.cpp | 40 ----- test/pyalps/loadobs.py | 27 ---- test/pyalps/loadobs_test.py | 142 ++++++++++++++++++ test/pyalps/numpylarge.py | 30 ---- test/pyalps/numpylarge_test.py | 87 +++++++++++ test/pyalps/test_binding_surface.py | 126 ++++++++++++++++ test/pyalps/tutorials_test.py | 78 ++++++++++ tutorials/ngs/6_python_native/ising.py | 35 +++-- tutorials/ngs/6_python_native/main.py | 2 +- tutorials/ngs/6_python_native/smoke_test.py | 81 ++++++++++ tutorials/ngs/7_python_extend/ising.py | 16 +- tutorials/ngs/7_python_extend/main.py | 2 +- tutorials/ngs/7_python_extend/smoke_test.py | 103 +++++++++++++ 31 files changed, 963 insertions(+), 284 deletions(-) create mode 100644 bindings/python/pyalps/cpp/archive_savable.hpp delete mode 100644 bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp delete mode 100644 src/alps/python/make_copy.hpp delete mode 100644 test/pyalps/accumulators.py create mode 100644 test/pyalps/accumulators_test.py delete mode 100644 test/pyalps/loadobs.cpp delete mode 100644 test/pyalps/loadobs.py create mode 100644 test/pyalps/loadobs_test.py delete mode 100644 test/pyalps/numpylarge.py create mode 100644 test/pyalps/numpylarge_test.py create mode 100644 test/pyalps/tutorials_test.py create mode 100644 tutorials/ngs/6_python_native/smoke_test.py create mode 100644 tutorials/ngs/7_python_extend/smoke_test.py diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index edf9dbe65..a59ccd632 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -194,10 +194,20 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/" DESTINATION pyalps FILES_MATCHING PATTERN "*.py") -# Runtime fallback paths pointing at the ALPS SDK this build used. The -# in-package pyalps/xml and pyalps/bin directories take precedence in -# pyalps.tools when they exist. -set(PYALPS_ALPS_ROOT "${ALPS_ROOT_DIR}") +# Runtime fallback paths for pyalps.tools, used only when the package does +# not carry the resource in question. The XML/XSL library below is always +# bundled, so its fallback is always empty; the application binaries are +# bundled unless PYALPS_BUNDLE_APPLICATIONS is OFF, and only that +# configuration needs to point back at the SDK that built the package. +# +# Never fill these in for a bundled package: they would be absolute paths on +# the build machine, baked into a redistributable artifact. +set(PYALPS_ALPS_XML_FALLBACK "") +if(PYALPS_BUNDLE_APPLICATIONS) + set(PYALPS_ALPS_BIN_FALLBACK "") +else() + set(PYALPS_ALPS_BIN_FALLBACK "${ALPS_ROOT_DIR}/bin") +endif() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/pyalps_config.py.in" "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" DESTINATION pyalps) diff --git a/bindings/python/pyalps/cpp/archive_savable.hpp b/bindings/python/pyalps/cpp/archive_savable.hpp new file mode 100644 index 000000000..6f1485a75 --- /dev/null +++ b/bindings/python/pyalps/cpp/archive_savable.hpp @@ -0,0 +1,36 @@ +// Copyright (C) 2026 by the ALPS collaboration +// Part of the ALPS Project — see LICENSE.txt for full license text. +// SPDX-License-Identifier: MIT +// +// Declares which bound types can be written with `archive[path] = object`. +// +// pyalps.hdf5's __setitem__ has to decide whether an object knows how to save +// itself or should be picked apart as a number / sequence / mapping. For +// objects defined in Python it duck-types on a bound save() method. That test +// cannot work for extension types: nanobind's bound methods are their own type +// (nb_bound_method), and more importantly having a method *named* save is not +// enough -- alps::alea's MCScalarData / MCVectorData and the pyalea +// observables all have one that takes a file name and an observable name +// instead of an archive, and calling those with an archive is a type error. +// +// So the capability is declared, not guessed, and it is declared at the +// binding site where the signature is known. The archive module then needs no +// knowledge of the types at all -- no includes, no enumeration to keep in sync +// as types are added. +#ifndef PYALPS_ARCHIVE_SAVABLE_HPP +#define PYALPS_ARCHIVE_SAVABLE_HPP +#include +namespace pyalps { +// Attribute set on the class object. Looked up on instances, so inheritance +// carries it to Python subclasses -- for those, getattr finds the subclass's +// own save() override, which is what should run. +inline constexpr char const * archive_savable_attr = "_alps_archive_savable"; +// Mark `cls` as having a save(alps::hdf5::archive &). Call it on every class +// that binds one; the class must also bind "save". Takes a handle so it +// accepts either a nb::class_ or the class looked up on the module, and no +// binding site has to be restructured to name its class object. +inline void mark_archive_savable(nanobind::handle cls) { + cls.attr(archive_savable_attr) = true; +} +} // namespace pyalps +#endif // PYALPS_ARCHIVE_SAVABLE_HPP diff --git a/bindings/python/pyalps/cpp/ngs/accumulator.cpp b/bindings/python/pyalps/cpp/ngs/accumulator.cpp index 9cb2824bf..5858e71b6 100644 --- a/bindings/python/pyalps/cpp/ngs/accumulator.cpp +++ b/bindings/python/pyalps/cpp/ngs/accumulator.cpp @@ -10,6 +10,12 @@ #include #include #include +// bind_serializable() below binds __str__ as a std::string-returning +// function, which needs the string caster in this translation unit -- +// without it every accumulator's and result's str() raised +// "Unable to convert function return value to a Python type". +#include +#include "../archive_savable.hpp" #include #include namespace nb = nanobind; @@ -68,6 +74,7 @@ void bind_serializable(nb::class_ & cls) { .def("save", &T::save) .def("load", &T::load) .def("reset", &T::reset); + pyalps::mark_archive_savable(cls); } } // namespace NB_MODULE(pyngsaccumulator_c, m) { diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 1121bd6b8..5d1d11463 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -33,6 +33,7 @@ #include #pragma GCC visibility pop #include "extract_from_pyobject.hpp" +#include "../archive_savable.hpp" #include "../numpy_compat.hpp" #include #include @@ -152,6 +153,15 @@ namespace alps { return kind == k; // legacy: mixed scalar kinds → group } }; + // The full save dispatch, defined below. Container children are + // routed back through it rather than straight into the visitor, so + // that an object nested in a list or dict is offered the same + // save() paths as one assigned at the top level -- otherwise + // `archive[p] = {'Energy': observable}` failed while + // `archive[p] = observable` worked. + void python_hdf5_save(alps::hdf5::archive & ar, + std::string const & path, + nb::handle data); // Save-side visitor: receives a concrete C++ value (or a // nb::list / nb::dict) from extract_from_pyobject_py11 and // writes it to the archive at `path`. @@ -254,8 +264,7 @@ namespace alps { Py_ssize_t i = 0; for (auto item : l) { std::string child = path + "/" + std::to_string(static_cast(i++)); - hdf5_save_py11_visitor child_visitor{ar, child}; - extract_from_pyobject_py11(child_visitor, item); + python_hdf5_save(ar, child, item); } } static bool is_ndarray(PyObject * raw) { @@ -365,50 +374,79 @@ namespace alps { for (auto item : d) { std::string key = nb::cast(nb::str(item.first)); std::string child = path + "/" + ar.encode_segment(key); - hdf5_save_py11_visitor child_visitor{ar, child}; - extract_from_pyobject_py11(child_visitor, item.second); + python_hdf5_save(ar, child, item.second); } } }; std::string python_hdf5_get_filename(alps::hdf5::archive & ar) { return ar.get_filename(); } - // Does `data` expose a save() written in Python (as opposed to one - // inherited from a bound C++ type)? The legacy build dispatched to - // obj.save(archive) here, but gated it on the bound method's type - // name being "instancemethod" -- a Python 2 spelling, so the branch - // was dead on Python 3 and `ar["/"] = simulation` raised - // "Unsupported type" instead of checkpointing the object. Gate on - // types.MethodType instead, which is the Python 3 equivalent and, - // like the original, does not match nanobind's own method objects -- - // so registered extension types keep their native save path. - bool has_python_save_method(nb::handle data) { + // Run `save` with the archive's context moved to `path`, then restore + // it -- the calling convention every ALPS save() expects, since they + // write relative to the current context. Shared by the Python-object + // and declared-capability branches of python_hdf5_save below. + template + void save_in_path_context(alps::hdf5::archive & ar, + std::string const & path, + Save && save) { + std::string context = ar.get_context(); + ar.set_context(ar.complete_path(path)); + try { + save(); + } catch (...) { + ar.set_context(context); + throw; + } + ar.set_context(context); + } + // Can `data` save itself into an archive? + // + // Two ways to qualify. An object defined in Python is duck-typed on a + // bound save() method, as the legacy build did -- except that it gated + // on the method's type name being "instancemethod", a Python 2 + // spelling, so the branch was dead on Python 3 and + // `ar["/"] = simulation` raised "Unsupported type" instead of + // checkpointing the object. "method" is the Python 3 equivalent. + // + // An extension type qualifies by declaring the capability at its + // binding site with pyalps::mark_archive_savable (see + // ../archive_savable.hpp). Duck-typing cannot work there: nanobind's + // bound methods are their own type, and a save() that takes + // (filename, observable_name) rather than an archive -- as the + // alps::alea observables' does -- must not be called with one. + bool has_archive_save_method(nb::handle data) { if (!nb::hasattr(data, "save")) return false; + if (nb::hasattr(data, pyalps::archive_savable_attr)) + return true; nb::object attr = nb::getattr(data, "save"); - // A bound method defined in Python has tp_name "method"; the - // legacy code compared against "instancemethod", the Python 2 - // spelling. Compare the type name rather than calling - // PyMethod_Check: nanobind links extensions against a restricted - // CPython symbol list that does not export PyMethod_Type. This - // also matches the is_ndarray() check above. + // Compare the type name rather than calling PyMethod_Check: + // nanobind links extensions against a restricted CPython symbol + // list that does not export PyMethod_Type. This also matches the + // is_ndarray() check above. return std::strcmp(Py_TYPE(attr.ptr())->tp_name, "method") == 0; } void python_hdf5_save(alps::hdf5::archive & ar, std::string const & path, nb::handle data) { - if (has_python_save_method(data)) { - std::string context = ar.get_context(); - ar.set_context(ar.complete_path(path)); - try { + if (has_archive_save_method(data)) { + save_in_path_context(ar, path, [&] { nb::getattr(data, "save")(nb::cast(&ar, nb::rv_policy::reference)); - } catch (...) { - ar.set_context(context); - throw; - } - ar.set_context(context); + }); return; } + // A type that has a save() but did not declare it archive-shaped + // -- alps::alea's MCScalarData / MCVectorData and the pyalea + // observables, whose save() opens a file of its own. Naming the + // spelling that works beats letting the visitor below report the + // type as merely "Unsupported": the operation the user wants + // exists. + if (nb::hasattr(data, "save")) + throw nb::type_error( + "this object's save() takes a file name rather than an " + "archive, so it cannot be stored with " + "archive[path] = object. Call object.save(filename, path) " + "instead."); hdf5_save_py11_visitor visitor{ar, path}; extract_from_pyobject_py11(visitor, data); } diff --git a/bindings/python/pyalps/cpp/ngs/mcbase.cpp b/bindings/python/pyalps/cpp/ngs/mcbase.cpp index 5cb421eb4..a279719da 100644 --- a/bindings/python/pyalps/cpp/ngs/mcbase.cpp +++ b/bindings/python/pyalps/cpp/ngs/mcbase.cpp @@ -43,6 +43,7 @@ #include #include #include +#include "../archive_savable.hpp" #include #include #include @@ -143,4 +144,5 @@ NB_MODULE(pyngsbase_c, m) { .def("load", [](alps::mcbase & self, alps::hdf5::archive & ar) { self.alps::mcbase::load(ar); }); + pyalps::mark_archive_savable(m.attr("mcbase")); } diff --git a/bindings/python/pyalps/cpp/ngs/observable.cpp b/bindings/python/pyalps/cpp/ngs/observable.cpp index 537aca696..45b52d0df 100644 --- a/bindings/python/pyalps/cpp/ngs/observable.cpp +++ b/bindings/python/pyalps/cpp/ngs/observable.cpp @@ -32,6 +32,7 @@ #include #include #include +#include "../archive_savable.hpp" #include #include #include @@ -117,4 +118,5 @@ NB_MODULE(pyngsobservable_c, m) { // load-compatibly) bound addToObservable to the same helper // as load. .def("addToObservable", &alps::detail::observable_load); + pyalps::mark_archive_savable(m.attr("observable")); } diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index ac02e8559..b16dfc872 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -39,6 +39,7 @@ #include #include #include +#include "../archive_savable.hpp" #include #include #include @@ -111,4 +112,5 @@ NB_MODULE(pyngsobservables_c, m) { nb::arg("name"), nb::arg("binnum") = 0) .def("createRealVectorObservable", &createRealVectorObservable, nb::arg("name"), nb::arg("binnum") = 0); + pyalps::mark_archive_savable(m.attr("observables")); } diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index ff575a6b2..a8d6c0807 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "../archive_savable.hpp" #include #include #include @@ -111,4 +112,5 @@ NB_MODULE(pyngsparams_c, m) { .def("load", ¶ms_load, nb::arg("archive"), nb::arg("path") = std::string("/parameters")); + pyalps::mark_archive_savable(m.attr("params")); } diff --git a/bindings/python/pyalps/cpp/ngs/random01.cpp b/bindings/python/pyalps/cpp/ngs/random01.cpp index 4efc47dee..0cd3310fa 100644 --- a/bindings/python/pyalps/cpp/ngs/random01.cpp +++ b/bindings/python/pyalps/cpp/ngs/random01.cpp @@ -6,6 +6,7 @@ #include #include #include +#include "../archive_savable.hpp" #include namespace nb = nanobind; NB_MODULE(pyngsrandom01_c, m) { @@ -21,4 +22,5 @@ NB_MODULE(pyngsrandom01_c, m) { &alps::random01::operator())) .def("save", &alps::random01::save) .def("load", &alps::random01::load); + pyalps::mark_archive_savable(m.attr("random01")); } diff --git a/bindings/python/pyalps/cpp/ngs/result.cpp b/bindings/python/pyalps/cpp/ngs/result.cpp index 5d7d6248e..11ad2ab9f 100644 --- a/bindings/python/pyalps/cpp/ngs/result.cpp +++ b/bindings/python/pyalps/cpp/ngs/result.cpp @@ -4,6 +4,7 @@ // Part of the ALPS Project — see LICENSE.txt for full license text. // SPDX-License-Identifier: MIT #include +#include "../archive_savable.hpp" #include #include #include @@ -178,4 +179,5 @@ NB_MODULE(pyngsresult_c, m) { .def("tanh", static_cast(&tanh)) .def("save", &R::save) .def("load", &R::load); + pyalps::mark_archive_savable(m.attr("result")); } diff --git a/bindings/python/pyalps/cpp/ngs/results.cpp b/bindings/python/pyalps/cpp/ngs/results.cpp index 6f4738630..77ef8d528 100644 --- a/bindings/python/pyalps/cpp/ngs/results.cpp +++ b/bindings/python/pyalps/cpp/ngs/results.cpp @@ -13,6 +13,7 @@ #include #include #include +#include "../archive_savable.hpp" #include #include #include @@ -68,4 +69,5 @@ NB_MODULE(pyngsresults_c, m) { .def("__str__", &alps::detail::mcresults_print) .def("save", &alps::mcresults::save) .def("load", &alps::detail::mcresults_load); + pyalps::mark_archive_savable(m.attr("results")); } diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp index 34756f9f8..1d87faa8a 100644 --- a/bindings/python/pyalps/cpp/pyalea.cpp +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -17,7 +17,7 @@ #include #include #include "numpy_compat.hpp" -#include "save_observable_to_hdf5.hpp" +#include #include #include #include diff --git a/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp b/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp deleted file mode 100644 index 556634686..000000000 --- a/bindings/python/pyalps/cpp/save_observable_to_hdf5.hpp +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) 2010 by Matthias Troyer , -// Part of the ALPS Project — see LICENSE.txt for full license text. -// SPDX-License-Identifier: MIT -#ifndef ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP -#define ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP -#include -namespace alps { namespace python { - - template void save_observable_to_hdf5(Obs const & obs, std::string const & filename) { - hdf5::archive ar(filename, "a"); - ar["/simulation/results/"+obs.representation()] << obs; - } - -} } // end namespace alps::python -#endif // ALPS_PYTHON_VERY_LONG_FILENAME_FOR_SAVE_OBSERVABLE_TO_HDF5_HPP diff --git a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in index 111f88995..c7e88c7c6 100644 --- a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in +++ b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in @@ -1,2 +1,13 @@ -ALPS_XML_INSTALL_DIR="@PYALPS_ALPS_ROOT@/lib/xml" -ALPS_BIN_INSTALL_DIR="@PYALPS_ALPS_ROOT@/bin" +# Generated from pyalps_config.py.in by bindings/python/pyalps/CMakeLists.txt. +# +# Last-resort fallbacks for the ALPS XML library and the application binaries, +# consulted by pyalps.tools only when the package does not carry them +# in-package as pyalps/xml and pyalps/bin. +# +# They are empty whenever the package does carry them, which is the default +# and what every published wheel uses. Filling them in unconditionally would +# bake absolute paths from the machine that built the wheel into the artifact: +# directories that exist nowhere else, that leak the build host's layout, and +# that make two builds of identical source differ. +ALPS_XML_INSTALL_DIR="@PYALPS_ALPS_XML_FALLBACK@" +ALPS_BIN_INSTALL_DIR="@PYALPS_ALPS_BIN_FALLBACK@" diff --git a/bindings/python/pyalps/src/pyalps/tools.py b/bindings/python/pyalps/src/pyalps/tools.py index 6d3a1c57b..35747b018 100644 --- a/bindings/python/pyalps/src/pyalps/tools.py +++ b/bindings/python/pyalps/src/pyalps/tools.py @@ -39,23 +39,32 @@ from . import alea import scipy.interpolate -if not "ALPS_XML_PATH" in os.environ: +def _packaged_or_configured_dir(name, configured): + """Locate an ALPS resource directory shipped with pyalps. + + The in-package copy (pyalps/) wins. `configured` is the fallback + baked in by CMake for builds that do not bundle the resource; it is empty + for a bundled package, in which case there is nothing to fall back to and + we leave the environment alone rather than exporting an empty path. + """ import pyalps - path = os.path.dirname(pyalps.__file__) + "/xml" + path = os.path.join(os.path.dirname(pyalps.__file__), name) if os.path.isdir(path): - os.environ["ALPS_XML_PATH"] = path - else: - from . import pyalps_config - os.environ["ALPS_XML_PATH"] = pyalps_config.ALPS_XML_INSTALL_DIR + return path + from . import pyalps_config + configured = getattr(pyalps_config, configured, "") + return configured if configured and os.path.isdir(configured) else None + + +if not "ALPS_XML_PATH" in os.environ: + _xml_path = _packaged_or_configured_dir("xml", "ALPS_XML_INSTALL_DIR") + if _xml_path is not None: + os.environ["ALPS_XML_PATH"] = _xml_path if not "ALPS_BIN_PATH" in os.environ: - import pyalps - path = os.path.dirname(pyalps.__file__) + "/bin" - if os.path.isdir(path): - os.environ["ALPS_BIN_PATH"] = path - else: - from . import pyalps_config - os.environ["ALPS_BIN_PATH"] = pyalps_config.ALPS_BIN_INSTALL_DIR + _bin_path = _packaged_or_configured_dir("bin", "ALPS_BIN_INSTALL_DIR") + if _bin_path is not None: + os.environ["ALPS_BIN_PATH"] = _bin_path def check_existence(cmd): @@ -66,14 +75,9 @@ def check_existence(cmd): if path_to_cmd is None: if cmd.startswith("/"): raise RuntimeError(f"There is no {cmd} on the path!") - import os - import pyalps - path = os.path.dirname(pyalps.__file__) + "/bin" - if os.path.isdir(path): + path = _packaged_or_configured_dir("bin", "ALPS_BIN_INSTALL_DIR") + if path is not None: os.environ["PATH"] += os.pathsep + path - else: - from . import pyalps_config - os.environ["PATH"] += os.pathsep + pyalps_config.ALPS_BIN_INSTALL_DIR if shutil.which(cmd) is None: raise RuntimeError(f"There is no {cmd} on the path!") diff --git a/src/alps/python/make_copy.hpp b/src/alps/python/make_copy.hpp deleted file mode 100644 index 4ba2e1a4a..000000000 --- a/src/alps/python/make_copy.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2010 by Matthias Troyer -// 2026 by the ALPS collaboration -// SPDX-License-Identifier: MIT -#ifndef ALPS_PYTHON_MAKE_COPY_HPP -#define ALPS_PYTHON_MAKE_COPY_HPP - -#include - -namespace alps { -namespace python { - -template -T make_copy(T const & value, nanobind::handle /*memo*/) { - return value; -} - -} // namespace python -} // namespace alps - -#endif diff --git a/test/pyalps/accumulators.py b/test/pyalps/accumulators.py deleted file mode 100644 index 744aa222d..000000000 --- a/test/pyalps/accumulators.py +++ /dev/null @@ -1,70 +0,0 @@ - -import numpy as np -from pyngsaccumulator_c import count_accumulator, mean_accumulator, error_accumulator, binning_analysis_accumulator, max_num_binning_accumulator - -def checkResult(r): - print(r) - r *= 2 - print(r) - print(-r) - print(r + 2) - print(2 + r) - print(r + r) - print(r - r) - print(r * r) - print(r / r) - print(np.sin(r)) - print(r.sin()) - print(r.cos()) - print(r.tan()) - print(r.sinh()) - print(r.cosh()) - print(r.tanh()) - # print r.asin() - # print r.acos() - # print r.atan() - print(r.abs()) - print(r.sqrt()) - print(r.log()) - # print r.sq() - # print r.cb() - # print r.cbrt() - -a = count_accumulator() -print(a) -a(1) -print(a) -checkResult(a.result()) - -b = mean_accumulator() -b(10) -print(b) -checkResult(b.result()) - -c = error_accumulator() -c(8) -c(12) -print(c) -checkResult(c.result()) - -d = binning_analysis_accumulator() -for i in range(1000): - d(float(i)) -print(d) -checkResult(d.result()) - -e = max_num_binning_accumulator() -for i in range(1000): - e(float(i)) -print(e) -r = e.result() -print(r) -# r *= 2 -print(r) -print(-r) -# print r + 2 -# print 2 + r -# print r + r -# print r - r -# print r * r -# print r / r diff --git a/test/pyalps/accumulators_test.py b/test/pyalps/accumulators_test.py new file mode 100644 index 000000000..16f86baf9 --- /dev/null +++ b/test/pyalps/accumulators_test.py @@ -0,0 +1,141 @@ +# **************************************************************************** +# +# ALPS Project: Algorithms and Libraries for Physics Simulations +# +# ALPS Libraries +# +# ALPS Project: https://alps.comp-phys.org/ +# SPDX-License-Identifier: MIT +# +# **************************************************************************** + +# Formerly accumulators.py: a print-only script importing the extension by its +# flat module name, which only resolved when tests ran with PYTHONPATH pointed +# at the old in-tree build directory. pytest never collected it (the name +# matched neither test_*.py nor *_test.py), so it had gone unrun long enough +# for str() on every accumulator to regress unnoticed. + +import numpy as np +import pytest + +from pyalps.cxx import pyngsaccumulator_c as accumulator + + +ACCUMULATORS = ( + "count_accumulator", + "mean_accumulator", + "error_accumulator", + "binning_analysis_accumulator", + "max_num_binning_accumulator", +) + + +def feed(name, samples): + """Build an accumulator of the named kind and feed it `samples`.""" + accum = getattr(accumulator, name)() + for sample in samples: + accum(float(sample)) + return accum + + +@pytest.mark.parametrize("name", ACCUMULATORS) +def test_accumulator_and_result_are_printable(name): + # __str__ is bound as a std::string-returning function; without + # nanobind/stl/string.h in that translation unit it raised TypeError + # instead of printing. + accum = feed(name, range(64)) + assert isinstance(str(accum), str) + assert str(accum) != "" + assert isinstance(str(accum.result()), str) + assert str(accum.result()) != "" + + +@pytest.mark.parametrize("name", ACCUMULATORS) +def test_count_survives_the_accumulator_to_result_transition(name): + accum = feed(name, range(37)) + assert accum.count() == 37 + assert accum.result().count() == 37 + + +@pytest.mark.parametrize( + "name", ("mean_accumulator", "error_accumulator", + "binning_analysis_accumulator", "max_num_binning_accumulator")) +def test_mean_matches_numpy(name): + samples = [1.5, 2.5, 3.5, 4.5] + accum = feed(name, samples) + assert accum.mean() == pytest.approx(np.mean(samples)) + assert accum.result().mean() == pytest.approx(np.mean(samples)) + + +def test_error_of_two_samples(): + # mean 10, sample standard error of {8, 12} is 2. + result = feed("error_accumulator", (8, 12)).result() + assert result.mean() == pytest.approx(10.0) + assert result.error() == pytest.approx(2.0) + + +def test_binning_analysis_reports_a_positive_error(): + for name in ("binning_analysis_accumulator", "max_num_binning_accumulator"): + result = feed(name, np.linspace(0.0, 1.0, 1024)).result() + assert result.mean() == pytest.approx(0.5, abs=1e-9) + assert result.error() > 0.0 + + +def test_result_arithmetic_against_scalars(): + result = feed("error_accumulator", (8, 12)).result() + + assert (result * 2).mean() == pytest.approx(20.0) + assert (result + 2).mean() == pytest.approx(12.0) + assert (2 + result).mean() == pytest.approx(12.0) + assert (result - 2).mean() == pytest.approx(8.0) + assert (2 - result).mean() == pytest.approx(-8.0) + assert (result / 2).mean() == pytest.approx(5.0) + assert (-result).mean() == pytest.approx(-10.0) + + # Non-mutating: the operators above take their operand by value. + assert result.mean() == pytest.approx(10.0) + + +def test_result_arithmetic_against_results(): + left = feed("error_accumulator", (8, 12)).result() + right = feed("error_accumulator", (3, 7)).result() + + assert (left + right).mean() == pytest.approx(15.0) + assert (left - right).mean() == pytest.approx(5.0) + assert (left * right).mean() == pytest.approx(50.0) + assert (left / right).mean() == pytest.approx(2.0) + + +def test_inplace_operators_mutate_and_return_the_same_object(): + result = feed("error_accumulator", (8, 12)).result() + identity = id(result) + + result *= 2 + assert result.mean() == pytest.approx(20.0) + result += 5 + assert result.mean() == pytest.approx(25.0) + result -= 5 + assert result.mean() == pytest.approx(20.0) + result /= 2 + assert result.mean() == pytest.approx(10.0) + + # rv_policy::none must hand back the original wrapper, not a copy. + assert id(result) == identity + + +def test_transcendental_functions_match_numpy(): + result = feed("mean_accumulator", (0.25,)).result() + + for name in ("sin", "cos", "tan", "sinh", "cosh", "tanh", "sqrt", "log", "abs"): + expected = getattr(np, name if name != "abs" else "fabs")(0.25) + assert getattr(result, name)().mean() == pytest.approx(expected), name + + # numpy ufuncs route through the bound methods for a scalar-like object. + assert np.sin(result).mean() == pytest.approx(np.sin(0.25)) + + +def test_reset_clears_the_samples(): + accum = feed("mean_accumulator", range(10)) + assert accum.count() == 10 + accum.reset() + assert accum.count() == 0 diff --git a/test/pyalps/loadobs.cpp b/test/pyalps/loadobs.cpp deleted file mode 100644 index c8b20473a..000000000 --- a/test/pyalps/loadobs.cpp +++ /dev/null @@ -1,40 +0,0 @@ -/***************************************************************************** -* -* ALPS Project: Algorithms and Libraries for Physics Simulations -* -* ALPS Libraries -* -* Copyright (C) 2010 by Lukas Gamper -* -* ALPS Project: https://alps.comp-phys.org/ -* SPDX-License-Identifier: MIT -* -*****************************************************************************/ - -#include -#include -#include - -#include -#include -#include -#include - - -int main() { - alps::hdf5::archive iar("loadobs.h5"); - - std::vector list = iar.list_children("/simulation/results"); - for (std::vector::const_iterator it = list.begin(); it != list.end(); ++it) { - iar.set_context("/simulation/results/" + iar.encode_segment(*it)); - if (iar.is_scalar("/simulation/results/" + iar.encode_segment(*it) + "/mean/value")) { - alps::alea::mcdata obs; - obs.load(iar); - std::cout << *it << " " << obs << std::endl; - } else { - alps::alea::mcdata > obs; - obs.load(iar); - std::cout << *it << " " << obs << std::endl; - } - } -} diff --git a/test/pyalps/loadobs.py b/test/pyalps/loadobs.py deleted file mode 100644 index fd5611981..000000000 --- a/test/pyalps/loadobs.py +++ /dev/null @@ -1,27 +0,0 @@ -# **************************************************************************** -# -# ALPS Project: Algorithms and Libraries for Physics Simulations -# -# ALPS Libraries -# -# Copyright (C) 2010 by Lukas Gamper -# Matthias Troyer -# -# ALPS Project: https://alps.comp-phys.org/ -# SPDX-License-Identifier: MIT -# -# **************************************************************************** - -import pyalps as alps -import pyalps.hdf5 as h5 -import pyalps.alea as alea - -iar = h5.archive('loadobs.h5', 'r') - -for name in iar.list_children('/simulation/results'): - if iar.is_scalar('/simulation/results/' + alps.hdf5_name_encode(name) + '/mean/value'): - obs = alea.MCScalarData() - else: - obs = alea.MCVectorData() - obs.load('loadobs.h5', '/simulation/results/' + alps.hdf5_name_encode(name)) - print(name + ": " + str(obs)) diff --git a/test/pyalps/loadobs_test.py b/test/pyalps/loadobs_test.py new file mode 100644 index 000000000..4827a2596 --- /dev/null +++ b/test/pyalps/loadobs_test.py @@ -0,0 +1,142 @@ +# **************************************************************************** +# +# ALPS Project: Algorithms and Libraries for Physics Simulations +# +# ALPS Libraries +# +# Copyright (C) 2010 by Lukas Gamper +# Matthias Troyer +# 2026 by the ALPS collaboration +# +# ALPS Project: https://alps.comp-phys.org/ +# SPDX-License-Identifier: MIT +# +# **************************************************************************** + +# Formerly loadobs.py (plus loadobs.cpp, its C++ twin). Both *read* a +# loadobs.h5 that neither of them created and that no longer ships anywhere in +# the tree, so both had been dead for years -- the Python half was additionally +# uncollectable by pytest, and the C++ half was orphaned when +# test/pyalps/CMakeLists.txt was removed with the in-tree bindings build. +# +# The surface they meant to cover is real: walk /simulation/results in an +# archive, decide scalar vs vector from the shape of mean/value, and load each +# entry into the matching mcdata type by path. This version builds its own +# fixture from pyalps.alea, so it needs no external file and no C++ helper. +# +# It also pins down the encoding rule both originals got wrong. list_children +# returns each name exactly as stored -- already HDF5-encoded -- so it belongs +# in a path verbatim, and hdf5_name_decode is what turns it back into the +# observable's real name. Both old versions instead re-encoded what +# list_children handed them (Python via hdf5_name_encode, C++ via +# encode_segment), which double-encodes any name containing '/' or '&': +# "Energy Density / Site" became "Energy Density &#47; Site" and the +# lookup missed. Their fixture evidently held only simple names. + +import numpy as np +import pytest + +import pyalps +import pyalps.alea as alea +import pyalps.hdf5 as hdf5 + + +RESULTS = "/simulation/results" + +SCALARS = { + "Energy": (-1.2345, 0.0067), + # '^' and '/' are exactly why hdf5_name_encode exists: they would + # otherwise be read as HDF5 path syntax. + "Magnetization^2": (0.3721, 0.0014), + "Energy Density / Site": (-0.6172, 0.0033), +} + +VECTORS = { + "Correlations": ([1.0, 0.5, 0.25], [0.01, 0.02, 0.03]), + "Green's Function": ([0.9, -0.4], [0.05, 0.06]), +} + + +@pytest.fixture +def results_archive(tmp_path): + """An archive holding scalar and vector observables under /simulation/results.""" + path = str(tmp_path / "loadobs.h5") + + for name, (mean, error) in SCALARS.items(): + observable = alea.MCScalarData(mean, error) + observable.save(path, RESULTS + "/" + pyalps.hdf5_name_encode(name)) + + for name, (mean, error) in VECTORS.items(): + observable = alea.MCVectorData(np.array(mean), np.array(error)) + observable.save(path, RESULTS + "/" + pyalps.hdf5_name_encode(name)) + + return path + + +def test_list_children_returns_stored_encoded_names(results_archive): + """Names come back encoded; decoding is what recovers the real name.""" + with hdf5.archive(results_archive, "r") as archive: + children = archive.list_children(RESULTS) + + expected = list(SCALARS) + list(VECTORS) + assert sorted(children) == sorted(pyalps.hdf5_name_encode(n) for n in expected) + assert sorted(pyalps.hdf5_name_decode(c) for c in children) == sorted(expected) + + # Re-encoding a name from list_children is the bug the originals had. + encoded_twice = pyalps.hdf5_name_encode("Energy Density / Site") + assert encoded_twice != "Energy Density / Site" + with hdf5.archive(results_archive, "r") as archive: + assert not archive.is_group(RESULTS + "/" + encoded_twice) + + +def test_name_encoding_roundtrips_through_the_archive(results_archive): + for name in list(SCALARS) + list(VECTORS): + encoded = pyalps.hdf5_name_encode(name) + assert pyalps.hdf5_name_decode(encoded) == name + with hdf5.archive(results_archive, "r") as archive: + assert archive.is_group(RESULTS + "/" + encoded) + + +def test_scalar_and_vector_observables_are_distinguishable(results_archive): + """The scalar/vector decision the original test drove its dispatch from.""" + with hdf5.archive(results_archive, "r") as archive: + for name in SCALARS: + path = RESULTS + "/" + pyalps.hdf5_name_encode(name) + "/mean/value" + assert archive.is_scalar(path), name + for name in VECTORS: + path = RESULTS + "/" + pyalps.hdf5_name_encode(name) + "/mean/value" + assert not archive.is_scalar(path), name + + +def test_load_dispatches_on_shape_and_restores_the_values(results_archive): + """The whole original loop, now asserting instead of printing.""" + with hdf5.archive(results_archive, "r") as archive: + loaded = {} + for stored in archive.list_children(RESULTS): + # `stored` is already encoded: use it verbatim in the path, and + # decode only to recover the observable's name. + path = RESULTS + "/" + stored + if archive.is_scalar(path + "/mean/value"): + observable = alea.MCScalarData() + else: + observable = alea.MCVectorData() + observable.load(results_archive, path) + loaded[pyalps.hdf5_name_decode(stored)] = observable + + assert sorted(loaded) == sorted(list(SCALARS) + list(VECTORS)) + + for name, (mean, error) in SCALARS.items(): + assert isinstance(loaded[name], alea.MCScalarData), name + assert loaded[name].mean == pytest.approx(mean), name + assert loaded[name].error == pytest.approx(error), name + + for name, (mean, error) in VECTORS.items(): + assert isinstance(loaded[name], alea.MCVectorData), name + np.testing.assert_allclose(loaded[name].mean, mean) + np.testing.assert_allclose(loaded[name].error, error) + + +def test_loaded_observables_are_printable(results_archive): + observable = alea.MCScalarData() + observable.load(results_archive, RESULTS + "/" + pyalps.hdf5_name_encode("Energy")) + assert "+/-" in str(observable) diff --git a/test/pyalps/numpylarge.py b/test/pyalps/numpylarge.py deleted file mode 100644 index 251750590..000000000 --- a/test/pyalps/numpylarge.py +++ /dev/null @@ -1,30 +0,0 @@ - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - # # - # ALPS Project: Algorithms and Libraries for Physics Simulations # - # # - # ALPS Libraries # - # # - # Copyright (C) 2010 - 2012 by Lukas Gamper # - # # - # ALPS Project: https://alps.comp-phys.org/ # - # SPDX-License-Identifier: MIT # - # # - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - -import numpy as np -import pyalps.hdf5 as hdf5 -import os - -ar=hdf5.archive('foo%d.h5', 'al') -s=2**10 - -while s < 2**29: - print(s) - a = np.empty(s) - ar[str(s)] = a - s *= 2 - -i = 0 -while os.path.isfile('foo%d.h5'%i): - os.remove('foo%d.h5'%i) - i += 1 diff --git a/test/pyalps/numpylarge_test.py b/test/pyalps/numpylarge_test.py new file mode 100644 index 000000000..5a9d56af0 --- /dev/null +++ b/test/pyalps/numpylarge_test.py @@ -0,0 +1,87 @@ +# **************************************************************************** +# +# ALPS Project: Algorithms and Libraries for Physics Simulations +# +# ALPS Libraries +# +# Copyright (C) 2010 - 2012 by Lukas Gamper +# 2026 by the ALPS collaboration +# +# ALPS Project: https://alps.comp-phys.org/ +# SPDX-License-Identifier: MIT +# +# **************************************************************************** + +# Formerly numpylarge.py: it doubled an array from 2**10 up to 2**29 doubles +# (4 GiB for the final write alone), printed the sizes, asserted nothing, and +# left its foo%d.h5 files in the working directory. pytest never collected it, +# so it also never cleaned up after itself. +# +# What it was actually exercising is worth keeping: the split-file archive +# ("foo%d.h5" plus mode "l"), which rolls over into foo0.h5, foo1.h5, ... once +# a member file passes the driver's threshold, and large contiguous writes +# through the numpy save path. Both are covered below at a size that belongs in +# a test suite, with assertions and in a temporary directory. + +import glob +import os + +import numpy as np +import pytest + +import pyalps.hdf5 as hdf5 + + +# 2**20 doubles = 8 MiB, enough to exercise a chunked write without making the +# suite slow. The original's 2**29 ceiling tested the operating system, not ALPS. +MAX_EXPONENT = 20 + + +@pytest.mark.parametrize("mode", ["a", "al"]) +def test_growing_arrays_roundtrip(tmp_path, mode): + """Doubling writes must read back byte-for-byte, plain or split-file.""" + pattern = str(tmp_path / "foo%d.h5") if "l" in mode else str(tmp_path / "foo.h5") + + written = {} + with hdf5.archive(pattern, mode) as archive: + size = 2 ** 10 + while size <= 2 ** MAX_EXPONENT: + values = np.linspace(0.0, 1.0, size) + archive[str(size)] = values + written[str(size)] = values + size *= 2 + + with hdf5.archive(pattern, "r" + ("l" if "l" in mode else "")) as archive: + for key, values in written.items(): + restored = archive[key] + assert restored.shape == values.shape, key + assert restored.dtype == values.dtype, key + np.testing.assert_array_equal(restored, values) + + +def test_split_archive_creates_numbered_member_files(tmp_path): + """Mode "l" must lay the data out across foo0.h5, foo1.h5, ... .""" + pattern = str(tmp_path / "foo%d.h5") + + with hdf5.archive(pattern, "al") as archive: + archive["/block"] = np.zeros(2 ** MAX_EXPONENT) + + members = sorted(glob.glob(str(tmp_path / "foo*.h5"))) + assert members, "the split-file driver wrote no member files" + assert os.path.basename(members[0]) == "foo0.h5" + + with hdf5.archive(pattern, "rl") as archive: + np.testing.assert_array_equal(archive["/block"], np.zeros(2 ** MAX_EXPONENT)) + + +def test_empty_and_single_element_arrays(tmp_path): + """The size-0 and size-1 edges of the same write path.""" + path = str(tmp_path / "edges.h5") + + with hdf5.archive(path, "a") as archive: + archive["/empty"] = np.empty(0) + archive["/single"] = np.array([np.pi]) + + with hdf5.archive(path, "r") as archive: + assert archive["/empty"].shape == (0,) + np.testing.assert_array_equal(archive["/single"], np.array([np.pi])) diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 986437562..40ac5524b 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -1075,6 +1075,128 @@ def GetProperties(self, filenames): assert apptest.checkProperties("test.h5", "reference.h5") +def test_archive_setitem_saves_registered_alps_types(): + """`archive[path] = obj` must reach the save() of a registered ALPS type. + + Each of these binds an archive-taking save(), but the __setitem__ gate only + recognised bound methods defined in Python -- nanobind's are a distinct + type -- so every one of them fell through to extract_from_pyobject, which + has no branch for them, and raised "Unsupported type" even though + obj.save(archive) worked when called directly. + """ + from pyalps import hdf5, ngs + + parameters = ngs.params({"L": 8, "T": 2.5, "MODEL": "spin"}) + observable = ngs.createRealObservable("Energy") + observable << 1.0 + observable << 2.0 + result = ngs.observable2result(observable) + container = ngs.observables() + container.createRealObservable("Magnetization") + container["Magnetization"] << 0.5 + + cases = { + "parameters": parameters, + "observable": observable, + "result": result, + "observables": container, + } + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "registered.h5") + with hdf5.archive(path, "w") as archive: + for key, value in cases.items(): + archive["/" + key] = value + + with hdf5.archive(path, "r") as archive: + # Each save() writes a group of its own children; an empty or + # missing group would mean the dispatch silently did nothing. + for key in cases: + assert archive.is_group("/" + key), key + assert archive.list_children("/" + key), key + assert sorted(archive.list_children("/parameters")) == ["L", "MODEL", "T"] + assert archive.list_children("/observables") == ["Magnetization"] + + restored = ngs.params() + with hdf5.archive(path, "r") as archive: + restored.load(archive, "/parameters") + assert int(restored["L"]) == 8 + assert float(restored["T"]) == 2.5 + assert str(restored["MODEL"]) == "spin" + + +def test_archive_setitem_reaches_registered_types_nested_in_containers(): + """A dict or list of ALPS objects is what a checkpoint actually looks like. + + Container children used to be handed straight to the save visitor, which + bypassed the save() dispatch entirely, so `archive[p] = {...}` failed for + exactly the values that worked at the top level. + """ + from pyalps import hdf5, ngs + + first = ngs.createRealObservable("Energy") + first << 1.0 + second = ngs.createRealObservable("Magnetization") + second << 0.25 + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "nested.h5") + with hdf5.archive(path, "w") as archive: + archive["/measurements"] = {"Energy": first, "Magnetization": second} + archive["/sequence"] = [ngs.params({"A": 1}), ngs.params({"B": 2})] + archive["/deep"] = {"clone": {"Energy": first}} + + with hdf5.archive(path, "r") as archive: + assert sorted(archive.list_children("/measurements")) == [ + "Energy", "Magnetization"] + assert archive.list_children("/measurements/Energy") + assert sorted(archive.list_children("/sequence")) == ["0", "1"] + assert archive.list_children("/sequence/0") == ["A"] + assert archive.list_children("/deep/clone/Energy") + + +def test_archive_setitem_rejects_mcdata_with_actionable_advice(): + """The one ALPS family whose save() is not archive-shaped. + + MCScalarData.save takes (filename, observable_name), so it is deliberately + excluded from the dispatch above -- but the message has to name the + spelling that does work rather than calling the type unsupported. + """ + from pyalps import alea, hdf5 + + observable = alea.MCScalarData(1.0, 0.1) + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "mcdata.h5") + with hdf5.archive(path, "w") as archive: + with pytest.raises(TypeError, match="save.filename, path."): + archive["/observable"] = observable + + # The documented spelling still works. + target = os.path.join(directory, "direct.h5") + observable.save(target, "/simulation/results/Energy") + restored = alea.MCScalarData() + restored.load(target, "/simulation/results/Energy") + assert restored.mean == pytest.approx(1.0) + + +def test_accumulator_results_are_printable(): + """__str__ returns std::string, which needs the caster in that module. + + Without nanobind/stl/string.h in accumulator.cpp, str() on all ten + accumulator and result types raised TypeError. + """ + from pyalps.cxx import pyngsaccumulator_c as accumulator + + for name in ("count_accumulator", "mean_accumulator", "error_accumulator", + "binning_analysis_accumulator", "max_num_binning_accumulator"): + accum = getattr(accumulator, name)() + for sample in range(16): + accum(float(sample)) + assert isinstance(str(accum), str) and str(accum), name + assert isinstance(str(accum.result()), str) and str(accum.result()), name + + if __name__ == "__main__": for test in ( test_extension_import_surface, @@ -1095,6 +1217,10 @@ def GetProperties(self, filenames): test_mapping_views_are_set_like, test_mcbase_save_load_overrides_reach_cpp_dispatch, test_accumulator_result_inplace_identity, + test_archive_setitem_saves_registered_alps_types, + test_archive_setitem_reaches_registered_types_nested_in_containers, + test_archive_setitem_rejects_mcdata_with_actionable_advice, + test_accumulator_results_are_printable, ): test() print("pyalps binding surface: green") diff --git a/test/pyalps/tutorials_test.py b/test/pyalps/tutorials_test.py new file mode 100644 index 000000000..4162e8998 --- /dev/null +++ b/test/pyalps/tutorials_test.py @@ -0,0 +1,78 @@ +# Copyright (C) 2026 by the ALPS collaboration +# Part of the ALPS Project — see LICENSE.txt for full license text. +# SPDX-License-Identifier: MIT + +"""Run the pure-Python ngs tutorials as part of the wheel test suite. + +tutorials/ngs/5_export_python is covered by build.yml, which compiles it as a +downstream CMake project. Its two pure-Python siblings needed no compiler and so +had no coverage at all -- and both were broken in ways only running them shows: +6_python_native could not store an ngs.params or its measurements dict through +`archive[path] = ...`, and neither one's load() had ever executed (one wrote to +the archive instead of reading from it, both called double()). + +Running them here rather than in build.yml is deliberate: they need an installed +pyalps, which is what this suite already has, so cibuildwheel exercises them +against every wheel it builds. +""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + + +TUTORIALS = Path(__file__).resolve().parents[2] / "tutorials" / "ngs" + +# 5_export_python is absent: its smoke test imports a compiled extension that +# only exists after the downstream CMake build in build.yml. +PURE_PYTHON_TUTORIALS = ["6_python_native", "7_python_extend"] + + +@pytest.mark.parametrize("tutorial", PURE_PYTHON_TUTORIALS) +def test_tutorial_smoke_test(tutorial, tmp_path): + directory = TUTORIALS / tutorial + script = directory / "smoke_test.py" + if not script.is_file(): + pytest.skip(f"{script} is not present in this tree") + + # Run from a scratch directory so nothing lands in the source tree, with + # the tutorial itself importable (each has its own ising.py). + environment = dict(os.environ) + environment["PYTHONPATH"] = os.pathsep.join( + [str(directory)] + ([environment["PYTHONPATH"]] + if environment.get("PYTHONPATH") else [])) + + completed = subprocess.run( + [sys.executable, str(script)], + cwd=str(tmp_path), + env=environment, + capture_output=True, + text=True, + timeout=900, + ) + + assert completed.returncode == 0, ( + f"{tutorial}/smoke_test.py failed\n" + f"--- stdout ---\n{completed.stdout}\n" + f"--- stderr ---\n{completed.stderr}") + assert completed.stdout.strip().endswith("ok"), completed.stdout + + +def test_every_pure_python_tutorial_has_a_smoke_test(): + """A new pure-Python tutorial should not be able to arrive untested.""" + if not TUTORIALS.is_dir(): + pytest.skip("tutorials/ngs is not present in this tree") + + untested = [] + for directory in sorted(TUTORIALS.iterdir()): + if not directory.is_dir() or not list(directory.glob("*.py")): + continue + if list(directory.glob("*.cpp")): + continue # compiled tutorials are covered by build.yml + if not (directory / "smoke_test.py").is_file(): + untested.append(directory.name) + + assert not untested, f"pure-Python tutorials without a smoke test: {untested}" diff --git a/tutorials/ngs/6_python_native/ising.py b/tutorials/ngs/6_python_native/ising.py index ca1e56219..579740691 100644 --- a/tutorials/ngs/6_python_native/ising.py +++ b/tutorials/ngs/6_python_native/ising.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MIT # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +import pyalps import pyalps.hdf5 as hdf5 import pyalps.ngs as ngs # move mcbase usw to pyalps.montecarlo import numpy as np @@ -71,14 +72,6 @@ def measure(self): def fraction_completed(self): return 0 if self.sweeps < self.thermalization_sweeps else (self.sweeps - self.thermalization_sweeps) / float(self.total_sweeps) - def save(self, filename): - with hdf5.archive(filename, 'w') as ar: - ar['/'] = self - - def load(self, filename): - with hdf5.archive(filename, 'r') as ar: - self = ar['/'] - def run(self, stopCallback): stopped = False while True: @@ -114,7 +107,10 @@ def save(self, ar): ar.set_context("checkpoint") ar["sweeps"] = self.sweeps ar["spins"] = self.spins - ar["engine"] = self.random + # random01.save() writes its state under /engine, so it + # takes the archive directly -- ar["engine"] = self.random would + # nest it a second level down at /engine/engine. + self.random.save(ar) ar.set_context(context); @@ -125,22 +121,25 @@ def save(self, ar): def load(self, ar): try: - - self.parameters.load(ar["/parameters"]) # TODO: do we want to load the parameters? + + self.parameters.load(ar, "/parameters") context = ar.context ar.set_context("/simulation/realizations/0/clones/0") - ar["measurements"] = self.measurements + # save() stored the measurements dict as a group with one + # HDF5-encoded child per observable; read each one back in place. + for name, observable in self.measurements.items(): + observable.load(ar, "measurements/" + pyalps.hdf5_name_encode(name)) - self.length = int(self.parameters["L"]); - self.thermalization_sweeps = int(self.parameters["THERMALIZATION"]); - self.total_sweeps = int(self.parameters["SWEEPS"]); - self.beta = 1. / double(self.parameters["T"]); + self.length = int(self.parameters["L"]) + self.thermalization_sweeps = int(self.parameters["THERMALIZATION"]) + self.total_sweeps = int(self.parameters["SWEEPS"]) + self.beta = 1. / float(self.parameters["T"]) ar.set_context("checkpoint") - self.sweeps = ar["sweeps"] + self.sweeps = int(ar["sweeps"]) self.spins = ar["spins"] - self.random.load(ar["engine"]) + self.random.load(ar) ar.set_context(context) diff --git a/tutorials/ngs/6_python_native/main.py b/tutorials/ngs/6_python_native/main.py index bdb8b0cf2..6f4444092 100644 --- a/tutorials/ngs/6_python_native/main.py +++ b/tutorials/ngs/6_python_native/main.py @@ -40,7 +40,7 @@ try: with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'r') as ar: sim.load(ar) - except ArchiveNotFound: pass + except hdf5.ArchiveNotFound: pass if limit == 0: sim.run(lambda: False) diff --git a/tutorials/ngs/6_python_native/smoke_test.py b/tutorials/ngs/6_python_native/smoke_test.py new file mode 100644 index 000000000..a91340ef7 --- /dev/null +++ b/tutorials/ngs/6_python_native/smoke_test.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Exercise a simulation written entirely in Python against pyalps.ngs. + +Unlike 7_python_extend, `ising.sim` here does not derive from ngs.mcbase: it +composes ngs.params, ngs.random01 and ngs.createRealObservable by hand and +implements the scheduler protocol (update / measure / fraction_completed / run) +in Python. Its save() and load() are the archive-object protocol -- pyalps +finds a Python-defined save() on the object and calls it with the archive. + +This tutorial had no test of any kind, and both halves of its checkpoint were +broken: save() could not store an ngs.params or the measurements dict through +`archive[path] = ...`, and load() wrote to the archive instead of reading from +it, passed a value where params.load wants an archive, and called double(). +""" + +import os +import tempfile + +import numpy as np + +import ising +import pyalps.hdf5 as hdf5 + + +PARAMETERS = {"L": 12, "THERMALIZATION": 5, "SWEEPS": 20, "T": 2.0, "SEED": 42} + +OBSERVABLES = ["Correlations", "Energy", "Magnetization", + "Magnetization^2", "Magnetization^4"] + + +simulation = ising.sim(PARAMETERS) + +assert int(simulation.parameters["L"]) == 12 +assert sorted(simulation.result_names()) == OBSERVABLES +assert simulation.fraction_completed() == 0.0 + +# run() returns True when it finished rather than being stopped. +assert simulation.run(lambda: False) +assert simulation.fraction_completed() >= 1.0 + +results = simulation.collectResults() +assert sorted(results) == OBSERVABLES +for name in OBSERVABLES: + assert results[name].count == PARAMETERS["SWEEPS"], name +assert -1.0 <= results["Energy"].mean <= 1.0 +assert len(results["Correlations"].mean) == PARAMETERS["L"] + +with tempfile.TemporaryDirectory() as directory: + checkpoint = os.path.join(directory, "ising.clone0.h5") + + # `archive['/'] = simulation` dispatches to the Python-defined save(), + # which in turn stores an ngs.params and a dict of observables. + with hdf5.archive(checkpoint, "w") as archive: + archive["/"] = simulation + + with hdf5.archive(checkpoint, "r") as archive: + assert sorted(archive.list_children("/parameters")) == [ + "L", "SEED", "SWEEPS", "T", "THERMALIZATION"] + clone = "/simulation/realizations/0/clones/0" + assert sorted(archive.list_children(clone + "/measurements")) == OBSERVABLES + assert sorted(archive.list_children(clone + "/checkpoint")) == [ + "engine", "spins", "sweeps"] + + restored = ising.sim(PARAMETERS) + with hdf5.archive(checkpoint, "r") as archive: + restored.load(archive) + + assert restored.sweeps == simulation.sweeps + np.testing.assert_array_equal(restored.spins, simulation.spins) + assert int(restored.parameters["L"]) == int(simulation.parameters["L"]) + + # The restored measurements carry the samples from before the checkpoint. + restored_results = restored.collectResults() + for name in OBSERVABLES: + assert restored_results[name].count == results[name].count, name + + # A reloaded RNG must continue the original stream, not restart it. + assert [restored.random() for _ in range(4)] == \ + [simulation.random() for _ in range(4)] + +print("native python simulation: ok") diff --git a/tutorials/ngs/7_python_extend/ising.py b/tutorials/ngs/7_python_extend/ising.py index 725ffd796..6022b2b87 100644 --- a/tutorials/ngs/7_python_extend/ising.py +++ b/tutorials/ngs/7_python_extend/ising.py @@ -79,13 +79,17 @@ def load(self, ar): try: ngs.mcbase.load(self, ar) - self.length = int(self.parameters["L"]); - self.thermalization_sweeps = int(self.parameters["THERMALIZATION"]); - self.total_sweeps = int(self.parameters["SWEEPS"]); - self.beta = 1. / double(self.parameters["T"]); + self.length = int(self.parameters["L"]) + self.thermalization_sweeps = int(self.parameters["THERMALIZATION"]) + self.total_sweeps = int(self.parameters["SWEEPS"]) + # double() is not a Python builtin; load() had never run. + self.beta = 1. / float(self.parameters["T"]) - self.sweeps = ar["/simulation/realizations/0/clones/0/sweeps"] - self.spins = ar["/simulation/realizations/0/clones/0/spins"] + # Read back from the paths save() actually writes: it puts both + # under .../clones/0/checkpoint, which load() omitted. + checkpoint = "/simulation/realizations/0/clones/0/checkpoint/" + self.sweeps = int(ar[checkpoint + "sweeps"]) + self.spins = ar[checkpoint + "spins"] except: traceback.print_exc(file=sys.stderr) diff --git a/tutorials/ngs/7_python_extend/main.py b/tutorials/ngs/7_python_extend/main.py index d84fdb000..0dc70fade 100644 --- a/tutorials/ngs/7_python_extend/main.py +++ b/tutorials/ngs/7_python_extend/main.py @@ -42,7 +42,7 @@ try: with hdf5.archive(outfile[0:outfile.rfind('.h5')] + '.clone0.h5', 'r') as ar: sim.load(ar) - except ArchiveNotFound: pass + except hdf5.ArchiveNotFound: pass if limit == 0: sim.run(lambda: False) diff --git a/tutorials/ngs/7_python_extend/smoke_test.py b/tutorials/ngs/7_python_extend/smoke_test.py new file mode 100644 index 000000000..bb91e4221 --- /dev/null +++ b/tutorials/ngs/7_python_extend/smoke_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Exercise a Python subclass of ngs.mcbase driving the C++ scheduler. + +This is the migration's headline capability: `ising.sim` derives from +ngs.mcbase, so C++ calls back into Python for update / measure / +fraction_completed through nanobind's trampoline, while parameters, the RNG and +the measurement container come from the C++ base. + +Its save()/load() also exercise cooperative checkpointing -- the subclass calls +ngs.mcbase.save(self, ar) for the base state and then adds its own. That only +works because the base save is bound with a qualified call: dispatching it +through the vtable would re-enter the subclass override and run the body twice. + +The tutorial had no test, and load() had never run: it called double() and read +its two values from paths that save() does not write. +""" + +import os +import tempfile + +import numpy as np + +import ising +import pyalps.hdf5 as hdf5 +import pyalps.ngs as ngs + + +PARAMETERS = {"L": 12, "THERMALIZATION": 5, "SWEEPS": 20, "T": 2.0} + +OBSERVABLES = ["Correlations", "Energy", "Magnetization", + "Magnetization^2", "Magnetization^4"] + +CLONE = "/simulation/realizations/0/clones/0" + + +simulation = ising.sim(PARAMETERS, 42) + +# The subclass really is an mcbase, and the base descriptors serve it. +assert isinstance(simulation, ngs.mcbase) +assert int(simulation.parameters["L"]) == 12 +assert sorted(simulation.measurements) == OBSERVABLES +assert 0.0 <= simulation.random() < 1.0 +assert simulation.fraction_completed() == 0.0 + +# run() drives update()/measure() in Python from the C++ scheduler. +assert simulation.run(lambda: False) +assert simulation.fraction_completed() >= 1.0 +assert simulation.sweeps == PARAMETERS["THERMALIZATION"] + PARAMETERS["SWEEPS"] + +results = ngs.collectResults(simulation) +assert sorted(results) == OBSERVABLES +for name in OBSERVABLES: + assert results[name].count == PARAMETERS["SWEEPS"], name +assert -1.0 <= results["Energy"].mean <= 1.0 +assert len(results["Correlations"].mean) == PARAMETERS["L"] + +# A stop callback that fires immediately must report "stopped", not "finished". +stopped = ising.sim(PARAMETERS, 42) +assert not stopped.run(lambda: True) + +with tempfile.TemporaryDirectory() as directory: + checkpoint = os.path.join(directory, "ising.clone0.h5") + + with hdf5.archive(checkpoint, "w") as archive: + archive["/"] = simulation + + with hdf5.archive(checkpoint, "r") as archive: + # ngs.mcbase.save writes relative to the archive's context, which is + # "/" here, so the base state lands at the root -- while the subclass + # writes its own two values at absolute paths under the clone. The + # asymmetry is the tutorial's; load() reads each back from where it + # was written. + assert sorted(archive.list_children("/parameters")) == [ + "L", "SWEEPS", "T", "THERMALIZATION"] + assert sorted(archive.list_children("/measurements")) == OBSERVABLES + assert archive.is_group("/checkpoint/engine") + assert int(archive[CLONE + "/checkpoint/sweeps"]) == simulation.sweeps + np.testing.assert_array_equal( + archive[CLONE + "/checkpoint/spins"], simulation.spins) + + restored = ising.sim(PARAMETERS, 42) + with hdf5.archive(checkpoint, "r") as archive: + restored.load(archive) + + assert restored.sweeps == simulation.sweeps + np.testing.assert_array_equal(restored.spins, simulation.spins) + assert restored.length == simulation.length + assert restored.beta == simulation.beta + + restored_results = ngs.collectResults(restored) + assert sorted(restored_results) == OBSERVABLES + for name in OBSERVABLES: + assert restored_results[name].count == results[name].count, name + + # Results also go out through the public writer the tutorial uses. + measurements = os.path.join(directory, "ising.h5") + with hdf5.archive(measurements, "w") as archive: + ngs.saveResults(restored_results, restored.parameters, + archive, "/simulation/results") + with hdf5.archive(measurements, "r") as archive: + assert sorted(archive.list_children("/simulation/results")) == OBSERVABLES + +print("python subclass of mcbase: ok") From d027608e9540d8a63aea1a0668b49912d3727e9e Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 20 Aug 2026 23:52:36 -0500 Subject: [PATCH 34/52] fix(pyalps): keep the archive-savable marker honest downstream Marking mcbase archive-savable interacts with its deliberately non-virtual bound save(), which exists so a Python subclass calling super().save(ar) does not re-enter its own override. If a downstream C++ simulation inherited that base save through the marker, `archive['/'] = sim` would silently checkpoint base state only -- a partial write where the previous behaviour was a loud "Unsupported type". It does not: export_sim_to_python binds save() on the derived type, so getattr finds the derived binding and both spellings write the same tree. Verified by building tutorials/ngs/5_export_python and comparing archive trees entry by entry -- 30 entries, identical, including the simulation's own checkpoint/sweeps and checkpoint/state. That comparison is now part of that tutorial's smoke test, which the gated downstream test already runs, so the guarantee cannot quietly lapse if the exporter stops binding save(). Also: the fall-through message no longer asserts that an unmarked type's save() takes a file name -- true for every such type in the tree, but misleading for a binding that merely forgot the marker -- and archive_savable.hpp records that it is pyalps-internal by design, with the reason a downstream module does not need it. Co-Authored-By: Claude Opus 5 --- .../python/pyalps/cpp/archive_savable.hpp | 8 ++++ bindings/python/pyalps/cpp/ngs/hdf5.cpp | 21 +++++----- test/pyalps/test_binding_surface.py | 2 +- tutorials/ngs/5_export_python/smoke_test.py | 40 +++++++++++++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/bindings/python/pyalps/cpp/archive_savable.hpp b/bindings/python/pyalps/cpp/archive_savable.hpp index 6f1485a75..36c02a40c 100644 --- a/bindings/python/pyalps/cpp/archive_savable.hpp +++ b/bindings/python/pyalps/cpp/archive_savable.hpp @@ -33,4 +33,12 @@ inline void mark_archive_savable(nanobind::handle cls) { cls.attr(archive_savable_attr) = true; } } // namespace pyalps +// Scope: pyalps-internal. This header is not installed, so a downstream +// nanobind module cannot mark its own types -- and does not need to. A class +// exported with ALPS_EXPORT_SIM_TO_PYTHON derives from alps::mcbase and +// inherits the marker from it, and export_sim_to_python binds save() on the +// derived type, so `archive[path] = simulation` reaches the derived save and +// writes the same tree that simulation.save(archive) does. Tutorial 5's +// smoke test asserts exactly that. Ship this header with the SDK only if a +// downstream type ever needs the marker without deriving from mcbase. #endif // PYALPS_ARCHIVE_SAVABLE_HPP diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 5d1d11463..4aa311a14 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -435,18 +435,19 @@ namespace alps { }); return; } - // A type that has a save() but did not declare it archive-shaped - // -- alps::alea's MCScalarData / MCVectorData and the pyalea - // observables, whose save() opens a file of its own. Naming the - // spelling that works beats letting the visitor below report the - // type as merely "Unsupported": the operation the user wants - // exists. + // A type that has a save() but did not declare it archive-shaped. + // In this tree that means alps::alea's MCScalarData / MCVectorData + // and the pyalea observables, whose save() opens a file of its own + // -- but it also catches a type whose binding simply forgot the + // marker, so the message says what it actually knows rather than + // asserting the signature. if (nb::hasattr(data, "save")) throw nb::type_error( - "this object's save() takes a file name rather than an " - "archive, so it cannot be stored with " - "archive[path] = object. Call object.save(filename, path) " - "instead."); + "this object has a save() but does not declare an " + "archive-shaped one, so it cannot be stored with " + "archive[path] = object. If its save() takes a file name " + "(as the pyalps.alea observables' does), call " + "object.save(filename, path) instead."); hdf5_save_py11_visitor visitor{ar, path}; extract_from_pyobject_py11(visitor, data); } diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 40ac5524b..8aff03cae 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -1169,7 +1169,7 @@ def test_archive_setitem_rejects_mcdata_with_actionable_advice(): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "mcdata.h5") with hdf5.archive(path, "w") as archive: - with pytest.raises(TypeError, match="save.filename, path."): + with pytest.raises(TypeError, match="does not declare an archive-shaped"): archive["/observable"] = observable # The documented spelling still works. diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py index bc4a7e381..20da2a40d 100644 --- a/tutorials/ngs/5_export_python/smoke_test.py +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -44,4 +44,44 @@ assert after["Magnetization"].count == before["Magnetization"].count assert after["Magnetization"].mean == before["Magnetization"].mean + # `archive[path] = simulation` must write exactly what simulation.save() + # writes. It reaches save() through the archive-savable marker that this + # class inherits from mcbase -- but mcbase's own bound save() is a + # deliberately *non-virtual* qualified call (so that a Python subclass + # calling super().save(ar) does not re-enter its own override). If the + # exporter ever stopped binding save() on the derived type, that inherited + # non-virtual base save is what would run, and this spelling would silently + # checkpoint the base state only -- a partial write where the previous + # behaviour was a loud "Unsupported type". Compare the two trees. + def entries(archive, path="/", found=None, depth=0): + found = [] if found is None else found + if depth > 12: + return found + for child in archive.list_children(path): + child_path = path.rstrip("/") + "/" + child + found.append(child_path) + if archive.is_group(child_path): + entries(archive, child_path, found, depth + 1) + return found + + trees = {} + for label, write in (("explicit", lambda sim, ar: sim.save(ar)), + ("setitem", lambda sim, ar: ar.__setitem__("/", sim))): + fresh = ising_c.sim(parameters) + assert fresh.run(lambda: False) + target = os.path.join(directory, label + ".h5") + with hdf5.archive(target, "w") as archive: + write(fresh, archive) + with hdf5.archive(target, "r") as archive: + trees[label] = sorted(entries(archive)) + + assert trees["setitem"] == trees["explicit"], ( + "archive['/'] = simulation wrote a different tree than " + "simulation.save(archive):\n" + f" only in explicit: {sorted(set(trees['explicit']) - set(trees['setitem']))}\n" + f" only in setitem: {sorted(set(trees['setitem']) - set(trees['explicit']))}") + # Guard against both spellings degenerating to base-only state. + assert any(entry.endswith("/checkpoint/sweeps") for entry in trees["setitem"]), \ + trees["setitem"] + print("downstream nanobind export: ok") From 400490bf397551a253721d9ef151d25d6e58539b Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 21 Aug 2026 00:20:28 -0500 Subject: [PATCH 35/52] perf(pyalps): hand NumPy the buffer C++ already owns The numpy output path allocated numpy.empty(shape, dtype=...) through the cached numpy module and memcpy'd into it. Every caller had already built a std::vector, so each conversion cost two copies and a Python round trip. nb::ndarray over a heap vector owned by a capsule costs one. Measured, best of seven, macOS arm64: MCVectorData.mean n=1000 0.55 -> 0.36 us -35% MCVectorData.mean n=100000 10.20 -> 9.67 us -5% archive load 1-D n=100000 232 -> 212 us -9% archive load 2-D 300x300 230 -> 205 us -11% Modest: the Python round trip was a fixed cost, so small arrays gain most, and archive loads sit on a ~170us floor set by HDF5 itself. Integers deliberately keep the old dtype-named route. nb::ndarray describes its buffer through DLPack, which encodes only "signed 64-bit" and so cannot distinguish `long` from `long long`. NumPy can: different dtype objects, different .char ('l' vs 'q'), different repr. Routing them through DLPack turned archive["/ints"] from int64 into longlong -- identical bits, but a visible change in what pyalps returns, which this migration has no business making. It was caught by pyhdf5io_test.py no longer reproducing master's reference output, and a compile-time remap cannot fix it because the distinction is erased at the DLPack boundary. Floating point and complex have one unambiguous dtype each, so they take the zero-copy route. Three tests added: returned arrays stay writable, C-contiguous and non-aliasing; an array outlives every C++ reference and the archive it came from, across heap churn and forced collection; and integer dtypes match element for element on .char and repr, not merely by == (which cannot see the long/long long difference that broke the reference output). Numeric fidelity re-verified: master's own test scripts still reproduce master's reference output byte for byte. Co-Authored-By: Claude Opus 5 --- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 4 +- bindings/python/pyalps/cpp/numpy_compat.hpp | 103 ++++++++++++------ bindings/python/pyalps/cpp/pyalea.cpp | 8 +- bindings/python/pyalps/cpp/pymcdata.cpp | 6 +- test/pyalps/test_binding_surface.py | 114 ++++++++++++++++++++ 5 files changed, 195 insertions(+), 40 deletions(-) diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 4aa311a14..caa9603a7 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -468,7 +468,7 @@ namespace alps { // (0, 2) and (2, 0) there is no payload to read: construct the // correctly shaped NumPy array directly. if (total == 0) { - return alps::python::make_numpy_array(nullptr, shape); + return alps::python::make_numpy_array(std::vector(), shape); } else if (shape.size() <= 1) { // vector overload works directly. ar[path] >> flat; @@ -477,7 +477,7 @@ namespace alps { // multi-dim dataset into a flat buffer. ar >> alps::make_pvp(path, flat.data(), shape); } - return alps::python::make_numpy_array(flat.data(), shape); + return alps::python::make_numpy_array(std::move(flat), shape); } nb::object python_hdf5_load_impl(alps::hdf5::archive & ar, std::string const & path); diff --git a/bindings/python/pyalps/cpp/numpy_compat.hpp b/bindings/python/pyalps/cpp/numpy_compat.hpp index abcff3c61..a6a7ce7fa 100644 --- a/bindings/python/pyalps/cpp/numpy_compat.hpp +++ b/bindings/python/pyalps/cpp/numpy_compat.hpp @@ -17,14 +17,15 @@ #include #include #include +#include #include #include namespace alps { namespace python { namespace nb_ = nanobind; - // numpy dtype strings, indexed by the corresponding C++ type. - // Used by make_numpy_array() / as_contiguous() to drive the - // numpy.empty(dtype=…) / numpy.ascontiguousarray(dtype=…) calls. + // numpy dtype strings, indexed by the corresponding C++ type. Used by + // as_contiguous() and by the integer arm of make_numpy_array, where + // the dtype has to be named exactly (see the comment there). template struct numpy_dtype; template <> struct numpy_dtype { static constexpr char const* name = "bool"; }; template <> struct numpy_dtype { static constexpr char const* name = "int8"; }; @@ -63,40 +64,80 @@ namespace alps { } return mod; } - // Allocates numpy.empty(shape, dtype=numpy_dtype::name) and - // memcpy's `data` (length = product(shape)) into it. Returns - // a writable numpy.ndarray. + // Hand a heap-owned std::vector to NumPy through nb::ndarray. The + // capsule keeps the vector alive for as long as the array (or any + // view of it) exists, so the data is not copied on the way out. + // + // This used to allocate numpy.empty(shape, dtype=...) through the + // cached numpy module and memcpy into it: two copies per conversion + // (callers build a vector, which was then copied again) plus a Python + // call. nb::ndarray hands NumPy the buffer C++ already owns. + // + // The returned array is writable: NumPy takes no ownership, only a + // reference to the capsule, matching what numpy.empty + memcpy + // produced. An empty shape yields a 0-d array, as numpy.empty(()) + // did. template - inline nb_::object make_numpy_array(T const* data, + inline nb_::object make_numpy_array(std::vector values, std::vector const& shape) { - nb_::handle np = numpy_module(); - nb_::tuple shape_tuple = nb_::steal(PyTuple_New(static_cast(shape.size()))); - if (!shape_tuple.is_valid()) - throw nb_::python_error(); - // PyTuple_SetItem (not the SET_ITEM macro): the macro pokes - // tuple internals directly and is unavailable under the - // limited API, which is otherwise within reach for these - // bindings. SetItem steals the reference to dim. - for (std::size_t i = 0; i < shape.size(); ++i) { - PyObject * dim = PyLong_FromUnsignedLongLong(shape[i]); - if (!dim) + if constexpr (std::is_integral_v) { + // Integers keep the numpy.empty + memcpy route. nb::ndarray + // describes the buffer through DLPack, which encodes only + // "signed 64-bit" and so cannot distinguish `long` from + // `long long`. NumPy can: they are different dtype objects + // with different .char ('l' vs 'q') and different reprs, and + // routing int64 through DLPack changed + // archive["/ints"] from int64 to longlong. Same bits, but a + // visible change in returned dtype, which this migration has + // no business making. Passing the dtype name keeps it exact. + nb_::handle np = numpy_module(); + nb_::tuple shape_tuple = nb_::steal( + PyTuple_New(static_cast(shape.size()))); + if (!shape_tuple.is_valid()) throw nb_::python_error(); - PyTuple_SetItem(shape_tuple.ptr(), static_cast(i), dim); + for (std::size_t i = 0; i < shape.size(); ++i) { + PyObject* dim = PyLong_FromUnsignedLongLong(shape[i]); + if (!dim) + throw nb_::python_error(); + PyTuple_SetItem(shape_tuple.ptr(), + static_cast(i), dim); + } + nb_::object array = np.attr("empty")( + shape_tuple, nb_::arg("dtype") = numpy_dtype::name); + if (!values.empty()) { + auto view = nb_::cast>(array); + std::memcpy(view.data(), values.data(), + values.size() * sizeof(T)); + } + return array; + } else { + // Floating point and complex have one unambiguous NumPy dtype + // each, so the zero-copy route is exact. + auto* owned = new std::vector(std::move(values)); + nb_::capsule owner(owned, [](void* pointer) noexcept { + delete static_cast*>(pointer); + }); + return nb_::cast(nb_::ndarray( + owned->data(), shape.size(), shape.data(), owner)); } - nb_::object arr = np.attr("empty")( - shape_tuple, nb_::arg("dtype") = numpy_dtype::name); - // Bridge the freshly-allocated numpy buffer through nb::ndarray - // to get a writable raw pointer. - auto nd = nb_::cast>(arr); - std::size_t total = 1; - for (auto s : shape) total *= s; - if (total > 0) - std::memcpy(nd.data(), data, total * sizeof(T)); - return arr; } template - inline nb_::object make_numpy_array(std::vector const& v) { - return make_numpy_array(v.data(), {v.size()}); + inline nb_::object make_numpy_array(std::vector values) { + std::size_t const size = values.size(); + return make_numpy_array(std::move(values), {size}); + } + // Copying entry point, for callers whose source is not a vector they + // can give up (a single stack value, or a null pointer standing for an + // empty extent). `data` may be null when the shape has no elements. + template + inline nb_::object make_numpy_array(T const* data, + std::vector const& shape) { + std::size_t total = 1; + for (auto extent : shape) total *= extent; + std::vector values(total); + if (total > 0 && data != nullptr) + std::memcpy(values.data(), data, total * sizeof(T)); + return make_numpy_array(std::move(values), shape); } // Strong-ref'd C-contiguous view onto a numpy array of dtype T. // The owner handle keeps the array alive for the lifetime of diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp index 1d87faa8a..c80383071 100644 --- a/bindings/python/pyalps/cpp/pyalea.cpp +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -62,7 +62,7 @@ namespace alps { std::vector tmp(n); for (std::size_t i = 0; i < n; ++i) tmp[i] = static_cast(v[i]); - return alps::python::make_numpy_array(tmp.data(), {n}); + return alps::python::make_numpy_array(std::move(tmp), {n}); } nb::object mean() const { return _to_numpy(obs.mean()); } nb::object error() const { return _to_numpy(obs.error()); } @@ -88,7 +88,7 @@ nb::object seq_to_numpy(Container const & v) { std::vector tmp(n); for (std::size_t i = 0; i < n; ++i) tmp[i] = static_cast(v[i]); - return alps::python::make_numpy_array(tmp.data(), {n}); + return alps::python::make_numpy_array(std::move(tmp), {n}); } // Copy a numpy array into a std::vector. Used when the // caller still instantiates mctimeseries from a @@ -154,7 +154,7 @@ nb::object ts_to_numpy_vector_rows(TS const & ts) { auto const & rows = ts.timeseries(); if (rows.empty()) return alps::python::make_numpy_array( - static_cast(nullptr), {std::size_t{0}, std::size_t{0}}); + std::vector(), {std::size_t{0}, std::size_t{0}}); std::size_t nrows = rows.size(); std::size_t ncols = rows.front().size(); for (auto const & row : rows) @@ -166,7 +166,7 @@ nb::object ts_to_numpy_vector_rows(TS const & ts) { for (std::size_t j = 0; j < ncols; ++j) *dst++ = static_cast(row[j]); } - return alps::python::make_numpy_array(flat.data(), {nrows, ncols}); + return alps::python::make_numpy_array(std::move(flat), {nrows, ncols}); } } // namespace NB_MODULE(pyalea_c, m) { diff --git a/bindings/python/pyalps/cpp/pymcdata.cpp b/bindings/python/pyalps/cpp/pymcdata.cpp index 017299470..9466d8d9f 100644 --- a/bindings/python/pyalps/cpp/pymcdata.cpp +++ b/bindings/python/pyalps/cpp/pymcdata.cpp @@ -23,8 +23,8 @@ namespace alps { // std::vector. Allocates a fresh array via // numpy.empty + memcpy through the buffer protocol — no // numpy headers. - inline nb::object vec_to_numpy(std::vector const & v) { - return make_numpy_array(v.data(), {v.size()}); + inline nb::object vec_to_numpy(std::vector v) { + return make_numpy_array(std::move(v)); } // Build a 2-D numpy.ndarray from a vector>. // Rows must be equal-length; on mismatch throw a value error @@ -42,7 +42,7 @@ namespace alps { std::copy(row.begin(), row.end(), dst); dst += cols; } - return make_numpy_array(flat.data(), {rows, cols}); + return make_numpy_array(std::move(flat), {rows, cols}); } // __repr__ for mcdata — " +/- ". template diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 8aff03cae..45e63456f 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -1197,6 +1197,117 @@ def test_accumulator_results_are_printable(): assert isinstance(str(accum.result()), str) and str(accum.result()), name +def test_numpy_arrays_are_writable_and_own_their_buffer(): + """Arrays handed to Python must stay writable and outlive their C++ owner. + + The output path builds nb::ndarray over a heap std::vector owned by a + capsule, rather than allocating numpy.empty and memcpy'ing into it. Two + ways that could corrupt results instead of failing loudly: the array + coming back read-only, or the vector being freed while NumPy still points + at it. + """ + import gc + + from pyalps import alea + + observable = alea.MCVectorData(np.array([1.0, 2.0, 3.0]), + np.array([0.1, 0.2, 0.3])) + mean = observable.mean + assert isinstance(mean, np.ndarray) + assert mean.flags.writeable + assert mean.flags.c_contiguous + assert mean.base is not None, "array does not keep its owner alive" + + mean[0] = 99.0 + assert mean[0] == 99.0 + # Each access copies out of C++; mutating one must not touch the observable. + assert observable.mean[0] == 1.0 + + def orphan(): + local = alea.MCVectorData(np.arange(1000, dtype=float), np.full(1000, 0.5)) + return local.mean + + survivor = orphan() + for _ in range(3): + gc.collect() + churn = [bytearray(1 << 20) for _ in range(16)] + del churn + gc.collect() + assert survivor[0] == 0.0 and survivor[999] == 999.0 + assert survivor.sum() == 499500.0 + survivor[500] = -1.0 + assert survivor[500] == -1.0 + + +def test_archive_arrays_preserve_shape_dtype_and_outlive_the_archive(): + """The HDF5 load path shares the same nb::ndarray helper.""" + import gc + + from pyalps import hdf5 + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "arrays.h5") + with hdf5.archive(path, "w") as archive: + archive["/vector"] = np.arange(6.0) + archive["/matrix"] = np.arange(6.0).reshape(2, 3) + archive["/complex"] = np.array([1 + 2j, 3 - 4j]) + archive["/ints"] = np.arange(4, dtype=np.int64) + archive["/empty"] = np.empty(0) + + with hdf5.archive(path, "r") as archive: + vector = archive["/vector"] + matrix = archive["/matrix"] + assert np.array_equal(vector, np.arange(6.0)) + assert vector.flags.writeable + assert matrix.shape == (2, 3) + assert np.array_equal(matrix, np.arange(6.0).reshape(2, 3)) + assert archive["/complex"].dtype == np.complex128 + assert np.array_equal(archive["/complex"], np.array([1 + 2j, 3 - 4j])) + assert archive["/ints"].dtype == np.int64 + assert archive["/empty"].shape == (0,) + + # The archive is closed and collected; the arrays must still be valid. + gc.collect() + assert np.array_equal(vector, np.arange(6.0)) + assert matrix.shape == (2, 3) + + +def test_integer_arrays_keep_their_exact_numpy_dtype(): + """Integer dtypes must come back exactly as before, not merely equal. + + The zero-copy output path describes its buffer through DLPack, which + encodes "signed 64-bit" and cannot distinguish `long` from `long long`. + NumPy can: different dtype objects, different .char ('l' vs 'q'), and a + different repr. Routing integers that way silently turned int64 into + longlong, which is why they keep the dtype-named path. + """ + from pyalps import hdf5 + + cases = { + "/i64": np.arange(3, dtype=np.int64), + "/u64": np.arange(3, dtype=np.uint64), + "/i32": np.arange(3, dtype=np.int32), + "/f64": np.arange(3, dtype=np.float64), + "/c128": np.array([1 + 2j], dtype=np.complex128), + } + + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "dtypes.h5") + with hdf5.archive(path, "w") as archive: + for key, value in cases.items(): + archive[key] = value + + with hdf5.archive(path, "r") as archive: + for key, value in cases.items(): + restored = archive[key] + # Not just ==: .char distinguishes long from long long, and + # repr() is what the reference outputs pin down. + assert restored.dtype.char == value.dtype.char, ( + f"{key}: dtype.char {restored.dtype.char!r} != " + f"{value.dtype.char!r}") + assert repr(restored) == repr(value), key + + if __name__ == "__main__": for test in ( test_extension_import_surface, @@ -1221,6 +1332,9 @@ def test_accumulator_results_are_printable(): test_archive_setitem_reaches_registered_types_nested_in_containers, test_archive_setitem_rejects_mcdata_with_actionable_advice, test_accumulator_results_are_printable, + test_numpy_arrays_are_writable_and_own_their_buffer, + test_archive_arrays_preserve_shape_dtype_and_outlive_the_archive, + test_integer_arrays_keep_their_exact_numpy_dtype, ): test() print("pyalps binding surface: green") From d2dd006de34b79691637b03603c328524bb325e2 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 21 Aug 2026 00:21:55 -0500 Subject: [PATCH 36/52] refactor(pyalps): drop the unreachable extension-import fallback cxx.py caught ImportError around its thirteen `from ._ext import ` lines and retried them as top-level modules -- the spelling that worked in the old in-tree build, where every .so sat side by side on PYTHONPATH. No installed layout can satisfy it, so the branch could only ever turn a real import failure into a second, more confusing one. Replaced with an explicit list and one importlib loop, so the module names are stated once rather than implied by a scan of locals() for keys ending in "_c". The sys.modules registration stays, and is now documented as load-bearing rather than incidental: pyalps' own alea, alea_detail, hdf5, pytools and ngs modules all import through `from .cxx. import ...`, which is submodule syntax and cannot resolve from attributes alone. Also drops `from __future__ import print_function` from mpi.py, a no-op on every interpreter pyalps supports. Co-Authored-By: Claude Opus 5 --- bindings/python/pyalps/src/pyalps/cxx.py | 70 ++++++++++++------------ bindings/python/pyalps/src/pyalps/mpi.py | 1 - 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/bindings/python/pyalps/src/pyalps/cxx.py b/bindings/python/pyalps/src/pyalps/cxx.py index f18d50da5..ade13872d 100644 --- a/bindings/python/pyalps/src/pyalps/cxx.py +++ b/bindings/python/pyalps/src/pyalps/cxx.py @@ -12,40 +12,42 @@ # **************************************************************************** -## The purpose of this script is to import the compiled modules both in the -## installed directory (relative to the script) and in the build dicrectory -## while testing (absolute modules, available via PYTHONPATH) +## Imports the compiled extensions from pyalps._ext and re-exports them, both +## as attributes of pyalps.cxx and as pyalps.cxx. module entries. +## +## The historic second half of this file caught ImportError and retried the +## same names as top-level modules, for the old in-tree build where every .so +## sat side by side on PYTHONPATH. No installed layout can satisfy that spelling, +## so the fallback could only ever mask a genuine import failure. -try: - from ._ext import pyalea_c - from ._ext import pymcdata_c - from ._ext import pyngsbase_c - from ._ext import pyngsapi_c - from ._ext import pyngshdf5_c - from ._ext import pyngsobservable_c - from ._ext import pyngsobservables_c - from ._ext import pyngsparams_c - from ._ext import pyngsrandom01_c - from ._ext import pyngsaccumulator_c - from ._ext import pyngsresult_c - from ._ext import pyngsresults_c - from ._ext import pytools_c -except ImportError: - import pyalea_c - import pymcdata_c - import pyngsbase_c - import pyngsapi_c - import pyngshdf5_c - import pyngsobservable_c - import pyngsobservables_c - import pyngsparams_c - import pyngsrandom01_c - import pyngsaccumulator_c - import pyngsresult_c - import pyngsresults_c - import pytools_c +# The compiled extensions, in the order pyalps.cxx exposes them. +_EXTENSIONS = ( + "pyalea_c", + "pymcdata_c", + "pytools_c", + "pyngsparams_c", + "pyngshdf5_c", + "pyngsbase_c", + "pyngsobservable_c", + "pyngsobservables_c", + "pyngsresult_c", + "pyngsresults_c", + "pyngsapi_c", + "pyngsrandom01_c", + "pyngsaccumulator_c", +) +import importlib import sys -for k in list(locals().keys()): - if k.endswith('_c'): - sys.modules["{}.cxx.{}".format(__package__, k)] = locals()[k] + +for _name in _EXTENSIONS: + _module = importlib.import_module("." + _name, __package__ + "._ext") + globals()[_name] = _module + # Register under pyalps.cxx. as well. This is not decoration: the + # package's own modules (alea, alea_detail, hdf5, pytools, ngs) import + # through `from .cxx. import ...`, which is submodule syntax and + # needs a sys.modules entry, as does any downstream code that spells it + # the same way. Attribute access alone would not serve either. + sys.modules["{}.cxx.{}".format(__package__, _name)] = _module + +del _name, _module diff --git a/bindings/python/pyalps/src/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py index 328c7904c..d8f1cfc10 100644 --- a/bindings/python/pyalps/src/pyalps/mpi.py +++ b/bindings/python/pyalps/src/pyalps/mpi.py @@ -13,7 +13,6 @@ that level of interoperability. """ -from __future__ import annotations import atexit as _atexit from functools import reduce as _python_reduce From 0ef02d721fdd593c7596687b9d4fdb63a2b3541e Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 21 Aug 2026 11:43:16 -0500 Subject: [PATCH 37/52] build(pyalps): inherit the version from the repository pyalps carried its own hard-coded version in pyproject.toml, a second number to bump beside ALPS_VERSION.txt and one that had already drifted from it: ALPS_VERSION.txt read 2.3.4 while pyproject read 2.3.4b1, with nothing tying the two together. The numeric version now comes from ALPS_VERSION.txt -- the same file cmake/ALPSVersion.cmake reads for ALPS_VERSION_CORE -- through an in-tree scikit-build-core dynamic-metadata provider. A prerelease label cannot live in that file, because project(VERSION ...) rejects a non-numeric version and neither find_package() matching nor the library SOVERSION has a notion of prerelease ordering; CMake therefore takes it from the ALPS_VERSION_PRERELEASE cache variable, and the Python build now takes it from the environment variable of the same name, using the same vocabulary translated to PEP 440 (beta.1 -> b1, rc.2 -> rc2, dev.3 -> .dev3). An unrecognised label fails the build rather than producing a surprising version. The wheels CI sets ALPS_VERSION_PRERELEASE=beta.1 in [tool.cibuildwheel.environment], so published wheels keep the 2.3.4b1 identity they have today; clearing that line publishes a final release. A plain local build now produces 2.3.4 rather than silently claiming to be a beta. The sdist is rooted at the pyalps directory and cannot reach the repository root, so it force-includes its own copy of ALPS_VERSION.txt; the provider looks in both places, the same repo-or-vendored split the CMake side already handles for the application sources. Verified in all three layouts: a repo wheel build (2.3.4, and 2.3.4b1 with the label), an sdist that derives its version with no repository above it, and a wheel built from that sdist. pyalps.__version__ is added, read from the installed distribution metadata so that it too is not a second copy. test_wheel_payload.py fails if the installed version and ALPS_VERSION.txt ever disagree -- checked by temporarily setting the file to 9.9.9 and confirming the test goes red. One consequence is worth stating, and is documented in the pyalps README: because the number is inherited, a Python-only API change cannot be signalled in the pyalps version alone. It would take a bump of ALPS_VERSION.txt, which moves the whole project. Co-Authored-By: Claude Opus 5 --- bindings/python/pyalps/README.md | 30 ++++ .../pyalps/_build_support/alps_version.py | 134 ++++++++++++++++++ bindings/python/pyalps/pyproject.toml | 16 ++- bindings/python/pyalps/src/pyalps/__init__.py | 10 ++ test/pyalps/test_wheel_payload.py | 29 ++++ 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 bindings/python/pyalps/_build_support/alps_version.py diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index d0d52e959..fa4dffd79 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -65,3 +65,33 @@ into neither of nanobind's special ABI modes: modules that derive from pyalps types, so an abi3 pyalps wheel would force every such consumer to use the limited API too. Per-version wheels preserve ordinary downstream extension interoperability. + +## Versioning + +pyalps does not carry a version of its own. The numeric version is read from +`ALPS_VERSION.txt` at the repository root — the same file +`cmake/ALPSVersion.cmake` reads for `ALPS_VERSION_CORE` — so a release bump is +one edit rather than two that can drift. `test/pyalps/test_wheel_payload.py` +fails if the installed version and that file disagree. + +A prerelease label cannot live in that file: `project(VERSION ...)` rejects a +non-numeric version, and neither `find_package()` matching nor the library +SOVERSION has a notion of prerelease ordering. CMake takes it from the +`ALPS_VERSION_PRERELEASE` cache variable; the Python build takes it from the +environment variable of the same name, using the same vocabulary: + +| `ALPS_VERSION_PRERELEASE` | version with `ALPS_VERSION.txt` = 2.3.4 | +|---|---| +| unset | `2.3.4` | +| `beta.1` | `2.3.4b1` | +| `alpha.2` | `2.3.4a2` | +| `rc.1` | `2.3.4rc1` | +| `dev.3` | `2.3.4.dev3` | + +The wheels CI sets it in `[tool.cibuildwheel.environment]`; clear it there to +publish a final release. `python bindings/python/pyalps/_build_support/alps_version.py` +prints the version a build would produce. + +Note the consequence: because the number is inherited, a Python-only API change +cannot be signalled in the pyalps version alone — it takes a bump of +`ALPS_VERSION.txt`, which moves the whole project. diff --git a/bindings/python/pyalps/_build_support/alps_version.py b/bindings/python/pyalps/_build_support/alps_version.py new file mode 100644 index 000000000..496b0dde5 --- /dev/null +++ b/bindings/python/pyalps/_build_support/alps_version.py @@ -0,0 +1,134 @@ +# Copyright (C) 2026 by the ALPS collaboration +# SPDX-License-Identifier: MIT + +"""Derive the pyalps version from the repository, not from pyproject.toml. + +ALPS_VERSION.txt at the repository root is the single source of truth for the +release version; cmake/ALPSVersion.cmake reads the same file to set +ALPS_VERSION_CORE before ``project()``. This provider reads it for the Python +package metadata, so a release bump is one edit rather than two that can drift. + +A prerelease label is deliberately *not* in that file: ``project(VERSION ...)`` +rejects a non-numeric version, and neither ``find_package()`` matching nor the +library SOVERSION has a notion of prerelease ordering. CMake therefore takes it +from the ALPS_VERSION_PRERELEASE cache variable, set by the release process. The +environment variable of the same name is the equivalent here, using the same +vocabulary, translated to the PEP 440 spelling Python requires: + + (unset) -> 2.3.4 a final release + beta.1 -> 2.3.4b1 + alpha.2 -> 2.3.4a2 + rc.1 -> 2.3.4rc1 + dev.3 -> 2.3.4.dev3 + +Wired up in pyproject.toml as:: + + [project] + dynamic = ["version"] + + [[tool.dynamic-metadata]] + provider = { path = "_build_support", module = "alps_version" } +""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +#: Where ALPS_VERSION.txt can be, in the order tried. +#: +#: A build from the repository finds it three levels up. An sdist build finds it +#: at the root, because pyproject.toml force-includes it there -- the sdist is +#: rooted at this project directory and cannot reach outside itself, which is +#: also why the CMake side keeps its own repo-or-_vendor check. +_CANDIDATES = ( + Path("ALPS_VERSION.txt"), + Path("..") / ".." / ".." / "ALPS_VERSION.txt", +) + +_CORE = re.compile(r"^(?P[0-9]+\.[0-9]+\.[0-9]+)$") + +#: CMake's prerelease vocabulary mapped to PEP 440 separators. +_PRERELEASE_KINDS = { + "alpha": "a", + "a": "a", + "beta": "b", + "b": "b", + "rc": "rc", + "c": "rc", + "pre": "rc", + "preview": "rc", + "dev": ".dev", +} + +_PRERELEASE = re.compile(r"^(?P[A-Za-z]+)[.\-_]?(?P[0-9]+)?$") + + +def _read_core() -> str: + """Return MAJOR.MINOR.PATCH from ALPS_VERSION.txt.""" + for candidate in _CANDIDATES: + if not candidate.is_file(): + continue + text = candidate.read_text(encoding="utf-8").strip().splitlines() + core = text[0].strip() if text else "" + match = _CORE.match(core) + if match is None: + raise RuntimeError( + f"{candidate} must contain exactly MAJOR.MINOR.PATCH, but reads " + f"{core!r}. A prerelease label belongs in the " + f"ALPS_VERSION_PRERELEASE environment variable, and the leading " + f"'v' of a release tag is not part of the version." + ) + return match.group("core") + + tried = ", ".join(str(path) for path in _CANDIDATES) + raise RuntimeError( + "Cannot find ALPS_VERSION.txt, which supplies the pyalps version. " + f"Looked in: {tried} (relative to {Path.cwd()}). A build from the ALPS " + "repository finds it at the repository root; an sdist carries a copy, " + "placed there by the sdist.force-include entry in pyproject.toml." + ) + + +def _pep440_suffix(label: str) -> str: + """Translate a CMake prerelease label into its PEP 440 spelling.""" + label = label.strip() + if not label: + return "" + + match = _PRERELEASE.match(label) + if match is None: + raise RuntimeError( + f"ALPS_VERSION_PRERELEASE={label!r} is not a recognised label. " + f"Use a kind and a number, e.g. beta.1, rc.2 or dev.3; the " + f"supported kinds are {sorted(set(_PRERELEASE_KINDS))}." + ) + + kind = match.group("kind").lower() + if kind not in _PRERELEASE_KINDS: + raise RuntimeError( + f"ALPS_VERSION_PRERELEASE={label!r} has unknown kind {kind!r}. " + f"Supported kinds: {sorted(set(_PRERELEASE_KINDS))}." + ) + + return f"{_PRERELEASE_KINDS[kind]}{match.group('number') or '0'}" + + +def version() -> str: + """The full PEP 440 version for this build.""" + return _read_core() + _pep440_suffix(os.environ.get("ALPS_VERSION_PRERELEASE", "")) + + +def dynamic_metadata(settings, project): # noqa: ARG001 - provider protocol + """scikit-build-core dynamic-metadata 0.3 hook.""" + if settings: + raise RuntimeError( + "The alps_version provider takes no settings; the version comes " + "from ALPS_VERSION.txt and ALPS_VERSION_PRERELEASE." + ) + return {"version": version()} + + +if __name__ == "__main__": # a convenience for the release process + print(version()) diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index 802645978..432e61c0e 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -7,7 +7,12 @@ build-backend = "scikit_build_core.build" [project] name = "pyalps" -version = "2.3.4b1" +# Inherited from ALPS_VERSION.txt at the repository root, the same file +# cmake/ALPSVersion.cmake reads, so a release bump is one edit. A prerelease +# label comes from the ALPS_VERSION_PRERELEASE environment variable, matching +# how CMake takes it from the cache variable of that name. See +# _build_support/alps_version.py. +dynamic = ["version"] description = "Python Applications and Libraries for Physics Simulations" readme = "README.md" requires-python = ">=3.10" @@ -46,6 +51,9 @@ test = ["pytest>=8"] tests = ["pytest>=8", "coverage>=7", "pytest-benchmark>=5"] mpi = ["mpi4py>=4"] +[[tool.dynamic-metadata]] +provider = { path = "_build_support", module = "alps_version" } + [tool.scikit-build] cmake.source-dir = "." wheel.packages = ["src/pyalps"] @@ -64,6 +72,8 @@ ALPS_DIR = { env = "ALPS_DIR" } # be rebuilt from the sdist. [tool.scikit-build.sdist.force-include] "../../../LICENSE.txt" = "LICENSE.txt" +# The version file, so an sdist build can still derive its own version. +"../../../ALPS_VERSION.txt" = "ALPS_VERSION.txt" "../../../applications/dmft/qmc" = "_vendor/applications/dmft/qmc" "../../../tool" = "_vendor/tool" "../../../lib/xml" = "_vendor/lib/xml" @@ -78,6 +88,10 @@ test-command = "pytest -q {project}/test/pyalps" # CMake preset; ALPS_DIR points every wheel build at that install. The ccache # in _build/ccache spans the SDK and all wheel builds (cached across CI runs). [tool.cibuildwheel.environment] +# The release state of the wheels this CI publishes. The numeric version comes +# from ALPS_VERSION.txt; this is the prerelease label CMake would take from +# ALPS_VERSION_PRERELEASE. Clear it to publish a final release. +ALPS_VERSION_PRERELEASE = "beta.1" ALPS_DIR = "$(pwd)/_build/wheel-deps/install/share/alps" CCACHE_DIR = "$(pwd)/_build/ccache" CCACHE_NAMESPACE = "pyalps-wheel" diff --git a/bindings/python/pyalps/src/pyalps/__init__.py b/bindings/python/pyalps/src/pyalps/__init__.py index b3066bb35..201ff6180 100644 --- a/bindings/python/pyalps/src/pyalps/__init__.py +++ b/bindings/python/pyalps/src/pyalps/__init__.py @@ -12,6 +12,7 @@ # **************************************************************************** import sys +from importlib.metadata import PackageNotFoundError, version as _distribution_version from .dataset import * from .tools import * @@ -20,6 +21,15 @@ from . import fit_wrapper from . import cxx as cxx +# Read from the installed distribution rather than restated here: the version +# comes from ALPS_VERSION.txt at build time (see +# bindings/python/pyalps/_build_support/alps_version.py), and a second copy in +# the source would be a second thing to bump. +try: + __version__ = _distribution_version("pyalps") +except PackageNotFoundError: # an uninstalled source tree + __version__ = "0.0.0+unknown" + # The extensions live in ``pyalps._ext`` in wheels, but Boost.Python-era # installations also exposed the core modules directly below ``pyalps``. diff --git a/test/pyalps/test_wheel_payload.py b/test/pyalps/test_wheel_payload.py index 72fc5d080..3cce6abbe 100644 --- a/test/pyalps/test_wheel_payload.py +++ b/test/pyalps/test_wheel_payload.py @@ -151,3 +151,32 @@ def test_every_bundled_program_can_be_loaded(): ) assert not failures, "bundled programs that cannot start:\n " + "\n ".join(failures) + + +def test_version_is_inherited_from_the_repository(): + """pyalps' version must come from ALPS_VERSION.txt, not a second copy. + + The numeric core is read from that file at build time; a prerelease label, + which cannot live there because project(VERSION ...) rejects a non-numeric + version, comes from the ALPS_VERSION_PRERELEASE environment variable. So the + installed version must be the file's contents followed by nothing or by a + PEP 440 prerelease segment -- never a different number. + """ + import pyalps + + version_file = Path(__file__).resolve().parents[2] / "ALPS_VERSION.txt" + if not version_file.is_file(): + pytest.skip("not running from an ALPS checkout") + + core = version_file.read_text(encoding="utf-8").strip().splitlines()[0].strip() + assert re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", core), core + + installed = pyalps.__version__ + assert installed.startswith(core), ( + f"pyalps.__version__ is {installed!r} but ALPS_VERSION.txt says {core!r}; " + "the version is no longer inherited from the repository" + ) + suffix = installed[len(core):] + assert re.fullmatch(r"|(a|b|rc)[0-9]+|\.dev[0-9]+", suffix), ( + f"unexpected version suffix {suffix!r}" + ) From 5bf130b086c60f2e70431bd453257f900d5b8f26 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 17 Sep 2026 22:23:17 -0400 Subject: [PATCH 38/52] Fix nanobind mapping lifetimes and replacement semantics --- .../python/pyalps/cpp/mapping_lifetime.hpp | 39 ++++++ .../python/pyalps/cpp/ngs/observables.cpp | 19 ++- bindings/python/pyalps/cpp/ngs/params.cpp | 18 +-- bindings/python/pyalps/cpp/ngs/results.cpp | 22 ++-- test/pyalps/test_mapping_lifetimes.py | 114 ++++++++++++++++++ 5 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 bindings/python/pyalps/cpp/mapping_lifetime.hpp create mode 100644 test/pyalps/test_mapping_lifetimes.py diff --git a/bindings/python/pyalps/cpp/mapping_lifetime.hpp b/bindings/python/pyalps/cpp/mapping_lifetime.hpp new file mode 100644 index 000000000..8cf7cfe5e --- /dev/null +++ b/bindings/python/pyalps/cpp/mapping_lifetime.hpp @@ -0,0 +1,39 @@ +// Copyright (C) 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT +#ifndef PYALPS_MAPPING_LIFETIME_HPP +#define PYALPS_MAPPING_LIFETIME_HPP + +#include +#include +#include + +namespace pyalps { + +// A reference_internal lookup keeps its map alive, but an erased map node +// still dies immediately. Preserve that node for any Python references to +// its value, just as Boost.Python's indexing proxies did. Keeping the actual +// node (rather than returning copies on lookup) also preserves operations +// such as observable.merge() that can replace the value's internal payload. +template +void erase_map_item(Mapping & mapping, std::string const & key) { + namespace nb = nanobind; + if (!mapping.has(key)) + throw nb::key_error(key.c_str()); + + nb::object value = nb::cast(&mapping[key], nb::rv_policy::reference); + using node_type = typename Mapping::node_type; + auto pending = std::make_unique(); + nb::capsule owner(pending.get(), [](void * pointer) noexcept { + delete static_cast(pointer); + }); + node_type * node = pending.release(); + // Establish ownership before mutating the map, so even a Python + // allocation failure leaves every existing reference valid. Use the + // public call policy rather than nanobind's private keep-alive API. + nb::cpp_function([](nb::handle, nb::handle) {}, nb::keep_alive<1, 2>())( + value, owner); + *node = mapping.extract(key); +} + +} // namespace pyalps +#endif diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index b16dfc872..40db03d96 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -40,9 +40,9 @@ #include #include #include "../archive_savable.hpp" +#include "../mapping_lifetime.hpp" #include #include -#include #include namespace nb = nanobind; #include @@ -84,20 +84,19 @@ NB_MODULE(pyngsobservables_c, m) { }, nb::rv_policy::reference_internal) .def("__setitem__", [](alps::mcobservables & self, std::string const & k, alps::mcobservable const & v) { - self.insert(k, v); + if (self.has(k)) self[k] = v; + else self.insert(k, v); }) // mcobservables derives publicly from std::map; item deletion // restores what the legacy map_indexing_suite provided (and // what the MutableMapping mixins pop/popitem/clear need). - .def("__delitem__", [](alps::mcobservables & self, std::string const & k) { - if (!self.has(k)) - throw nb::key_error(k.c_str()); - self.erase(k); - }) + .def("__delitem__", &pyalps::erase_map_item) .def("__iter__", [](alps::mcobservables & self) { - return nb::make_key_iterator(nb::type(), "key_iterator", self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) + nb::list keys; + for (auto const & entry : self) + keys.append(nb::cast(entry.first)); + return keys.attr("__iter__")(); + }) // keys/values/items are deliberately NOT defined here. Boost.Python's // map_indexing_suite did not define them either, so they resolved through // MutableMapping to set-like KeysView/ValuesView/ItemsView. Defining them diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index a8d6c0807..0703aaf31 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -4,7 +4,6 @@ // Part of the ALPS Project — see LICENSE.txt for full license text. // SPDX-License-Identifier: MIT #include -#include #include #include #include @@ -98,14 +97,15 @@ NB_MODULE(pyngsparams_c, m) { .def("__delitem__", ¶ms_delitem) .def("__contains__", ¶ms_contains) .def("__iter__", [](alps::params & self) { - // paramiterator yields pair; - // make_key_iterator projects out pair.first. - return nb::make_key_iterator( - nb::type(), - "key_iterator", - self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) + // Snapshot keys: params' native iterator + // holds a vector iterator invalidated by + // insertion or deletion, even while the + // params object itself remains alive. + nb::list keys; + for (auto const & entry : self) + keys.append(nb::cast(entry.first)); + return keys.attr("__iter__")(); + }) .def("__str__", ¶ms_print) .def("valueOrDefault", &value_or_default) .def("save", &alps::params::save) diff --git a/bindings/python/pyalps/cpp/ngs/results.cpp b/bindings/python/pyalps/cpp/ngs/results.cpp index 77ef8d528..1663aeaed 100644 --- a/bindings/python/pyalps/cpp/ngs/results.cpp +++ b/bindings/python/pyalps/cpp/ngs/results.cpp @@ -9,11 +9,11 @@ // by hand instead. (Same applies to nanobind's bind_map.) #define PY_ARRAY_UNIQUE_SYMBOL pyngsresults_PyArrayHandle #include -#include #include #include #include #include "../archive_savable.hpp" +#include "../mapping_lifetime.hpp" #include #include #include @@ -46,20 +46,16 @@ NB_MODULE(pyngsresults_c, m) { }, nb::rv_policy::reference_internal) .def("__setitem__", [](alps::mcresults & self, std::string const & k, alps::mcresult const & v) { - self.insert(k, v); - }) - .def("__delitem__", [](alps::mcresults & self, std::string const & k) { - if (!self.has(k)) - throw nb::key_error(k.c_str()); - self.erase(k); + if (self.has(k)) self[k] = v; + else self.insert(k, v); }) + .def("__delitem__", &pyalps::erase_map_item) .def("__iter__", [](alps::mcresults & self) { - return nb::make_key_iterator( - nb::type(), - "key_iterator", - self.begin(), self.end()); - }, - nb::keep_alive<0, 1>()) + nb::list keys; + for (auto const & entry : self) + keys.append(nb::cast(entry.first)); + return keys.attr("__iter__")(); + }) // keys/values/items are deliberately NOT defined here. Boost.Python's // map_indexing_suite did not define them either, so they resolved through // MutableMapping to set-like KeysView/ValuesView/ItemsView. Defining them diff --git a/test/pyalps/test_mapping_lifetimes.py b/test/pyalps/test_mapping_lifetimes.py new file mode 100644 index 000000000..08e4ba9ad --- /dev/null +++ b/test/pyalps/test_mapping_lifetimes.py @@ -0,0 +1,114 @@ +"""Mapping mutation must not expose invalid C++ map nodes to Python.""" + +import os +import subprocess +import sys + +import pytest + + +_SETUP = """ +from pyalps import ngs + +class Simulation(ngs.mcbase): + def update(self): pass + def measure(self): pass + def fraction_completed(self): return 1.0 + +sim = Simulation({'SEED': 1}) +sim.measurements.createRealObservable('x') +sim.measurements['x'] << 1.0 << 3.0 +mapping = sim.measurements if kind == 'observables' else ngs.collectResults(sim) + +def mean(value): + return (ngs.observable2result(value) if kind == 'observables' else value).mean +""" + + +def _run(kind, code): + # A regression is a native use-after-free. Isolate it so a crash reports + # an ordinary test failure instead of taking down the whole suite. + completed = subprocess.run( + [sys.executable, "-c", f"kind = {kind!r}\n" + _SETUP + code], + env={**os.environ, "MallocScribble": "1"}, + capture_output=True, text=True, timeout=30, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + + +@pytest.mark.parametrize("kind", ["observables", "results"]) +@pytest.mark.parametrize("remove", ["del mapping['x']", "mapping.clear()", "mapping.pop('x')"]) +def test_values_survive_removal(kind, remove): + _run(kind, f""" +value = mapping['x'] +assert mean(value) == 2.0 +{remove} +assert mean(value) == 2.0 +del mapping, sim +import gc +gc.collect() +assert mean(value) == 2.0 +""") + + +@pytest.mark.parametrize("kind", ["observables", "results"]) +def test_pop_returns_usable_value(kind): + _run(kind, """ +value = mapping.pop('x') +assert mean(value) == 2.0 +assert not mapping +""") + + +@pytest.mark.parametrize("kind", ["observables", "results"]) +def test_assignment_and_update_replace_existing_values(kind): + _run(kind, """ +replacement = ngs.createRealObservable('replacement') +replacement << 7.0 << 9.0 +if kind == 'results': + replacement = ngs.observable2result(replacement) +mapping['x'] = replacement +assert mean(mapping['x']) == 8.0 +mapping.update({'x': replacement}) +assert mean(mapping['x']) == 8.0 +mapping['new'] = replacement +assert mean(mapping['new']) == 8.0 +""") + + +def test_observable_samples_still_update_the_container(): + _run('observables', """ +value = mapping['x'] +value << 8.0 +assert mean(mapping['x']) == 4.0 +""") + + +def test_observable_merge_still_updates_the_container(): + _run('observables', """ +other = ngs.createRealObservable('x') +other << 7.0 << 9.0 +mapping['x'].merge(other) +assert mean(mapping['x']) == 5.0 +""") + + +@pytest.mark.parametrize("kind", ["observables", "results"]) +def test_mapping_iterator_survives_mutation(kind): + _run(kind, """ +iterator = iter(mapping) +mapping.clear() +del mapping, sim +assert list(iterator) == ['x'] +""") + + +def test_params_iterator_survives_mutation(): + _run('observables', """ +parameters = ngs.params({'first': 1, 'second': 2}) +iterator = iter(parameters) +parameters.clear() +parameters['new'] = 3 +del parameters +assert list(iterator) == ['first', 'second'] +""") From 08ad15de9f279abcf9c4b9c624af0fd8e85caeae Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 17 Sep 2026 22:23:17 -0400 Subject: [PATCH 39/52] Test the requested Boost version in the compiler matrix --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6bff86717..b8995b955 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,7 +118,7 @@ jobs: CXX: ${{ matrix.plat.cxx_compiler }}-${{ matrix.plat.c_version }} run: | cmake -S $GITHUB_WORKSPACE -B build \ - -DBoost_ROOT_DIR=`pwd`/boost_1_${{ matrix.plat.boost_version }}_0 \ + -DBoost_SRC_DIR="$GITHUB_WORKSPACE/boost_1_${{ matrix.plat.boost_version }}_0" \ -DCMAKE_INSTALL_PREFIX="$GITHUB_WORKSPACE/build/install" \ -DCMAKE_CXX_STANDARD=${{ matrix.plat.cxx_standard || '17' }} \ -DCMAKE_CXX_FLAGS="-fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" @@ -183,7 +183,7 @@ jobs: cmake -S $GITHUB_WORKSPACE -B build \ -DCMAKE_C_COMPILER=${{ matrix.plat.c_compiler }} \ -DCMAKE_CXX_COMPILER=${{ matrix.plat.cxx_compiler }} \ - -DBoost_ROOT_DIR=`pwd`/boost_1_${{ matrix.plat.boost_version }}_0 \ + -DBoost_SRC_DIR="$GITHUB_WORKSPACE/boost_1_${{ matrix.plat.boost_version }}_0" \ -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_CXX_FLAGS="${{ matrix.plat.cxx_stdlib }} -fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" \ -DPython_ROOT_DIR=`$(brew --prefix)/bin/python${{ matrix.plat.py_version }} -c "import sys, os; print(os.path.dirname(os.path.dirname(str(sys.executable))));"` \ From c2621b0f9e21b3302fa9023a467442c27baf7820 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 17 Sep 2026 23:11:51 -0400 Subject: [PATCH 40/52] Preserve Python input conversions and validate vector construction --- .../python/pyalps/cpp/ngs/accumulator.cpp | 5 +- bindings/python/pyalps/cpp/ngs/observable.cpp | 2 +- bindings/python/pyalps/cpp/numpy_compat.hpp | 4 +- bindings/python/pyalps/cpp/pyalea.cpp | 13 ++- bindings/python/pyalps/cpp/pymcdata.cpp | 12 ++- test/pyalps/test_conversion_contracts.py | 94 +++++++++++++++++++ 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 test/pyalps/test_conversion_contracts.py diff --git a/bindings/python/pyalps/cpp/ngs/accumulator.cpp b/bindings/python/pyalps/cpp/ngs/accumulator.cpp index 5858e71b6..2b1e02f43 100644 --- a/bindings/python/pyalps/cpp/ngs/accumulator.cpp +++ b/bindings/python/pyalps/cpp/ngs/accumulator.cpp @@ -82,7 +82,10 @@ NB_MODULE(pyngsaccumulator_c, m) { using count_accumulator = Accumulator>; using count_result = count_accumulator::result_type; nb::class_ count_acc(m, "count_accumulator"); - count_acc.def(nb::init<>()).def("__call__", [](count_accumulator & self, double value) { self(value); }) + // A count-only accumulator never examines the sample. The old Python + // binding accepted any object, including arrays and complex samples. + count_acc.def(nb::init<>()).def("__call__", [](count_accumulator & self, nb::handle) { self(0.0); }, + nb::arg("sample").none()) .def("result", &make_result).def("count", &count_accumulator::count); bind_serializable(count_acc); nb::class_ count_res(m, "count_result"); diff --git a/bindings/python/pyalps/cpp/ngs/observable.cpp b/bindings/python/pyalps/cpp/ngs/observable.cpp index 45b52d0df..cf51449e4 100644 --- a/bindings/python/pyalps/cpp/ngs/observable.cpp +++ b/bindings/python/pyalps/cpp/ngs/observable.cpp @@ -51,7 +51,7 @@ namespace alps { return; } try { - auto values = nb::cast>(data); + auto values = nb::cast>(data); self << std::valarray(values.data(), values.size()); } catch (nb::cast_error const &) { throw nb::type_error("observable samples must be numeric scalars or contiguous float64 arrays"); diff --git a/bindings/python/pyalps/cpp/numpy_compat.hpp b/bindings/python/pyalps/cpp/numpy_compat.hpp index a6a7ce7fa..670378533 100644 --- a/bindings/python/pyalps/cpp/numpy_compat.hpp +++ b/bindings/python/pyalps/cpp/numpy_compat.hpp @@ -145,7 +145,7 @@ namespace alps { template struct contiguous_view { nb_::object owner; - nb_::ndarray nd; + nb_::ndarray nd; T const* data() const { return nd.data(); } std::size_t ndim() const { return nd.ndim(); } std::size_t shape(int i) const { return nd.shape(i); } @@ -162,7 +162,7 @@ namespace alps { nb_::handle np = numpy_module(); nb_::object arr = np.attr("ascontiguousarray")( obj, nb_::arg("dtype") = numpy_dtype::name); - auto nd = nb_::cast>(arr); + auto nd = nb_::cast>(arr); return contiguous_view{std::move(arr), std::move(nd)}; } } // namespace python diff --git a/bindings/python/pyalps/cpp/pyalea.cpp b/bindings/python/pyalps/cpp/pyalea.cpp index c80383071..3697e1cd5 100644 --- a/bindings/python/pyalps/cpp/pyalea.cpp +++ b/bindings/python/pyalps/cpp/pyalea.cpp @@ -73,7 +73,13 @@ namespace alps { ar["/simulation/results/" + obs.representation()] << obs; } typename T::count_type count() const { return obs.count(); } - typename T::convergence_type converged_errors() const { return obs.converged_errors(); } + nb::object converged_errors() const { + auto const values = obs.converged_errors(); + std::vector result(values.size()); + for (std::size_t i = 0; i < values.size(); ++i) + result[i] = values[i]; + return alps::python::make_numpy_array(std::move(result)); + } private: T obs; }; @@ -266,12 +272,12 @@ NB_MODULE(pyalea_c, m) { #define ALPS_PY_EXPORT_MCTIMESERIES_SCALAR(Value, PyName) \ nb::class_>(m, PyName) \ .def(nb::init<>()) \ + .def(nb::init>()) \ .def("__init__", \ [](alps::alea::mctimeseries * self, nb::handle a) { \ new (self) alps::alea::mctimeseries( \ numpy_to_vector(a)); \ }) \ - .def(nb::init>()) \ .def("timeseries", [](alps::alea::mctimeseries const & self) { \ return ts_to_numpy_scalar(self); \ }) \ @@ -294,6 +300,8 @@ NB_MODULE(pyalea_c, m) { using VecMcD = alps::alea::mcdata>; nb::class_(m, "MCVectorTimeseries") .def(nb::init<>()) + // Register typed overloads before the catch-all array constructor. + .def(nb::init()) .def("__init__", [](VecTs * self, nb::handle a) { auto view = alps::python::as_contiguous(a); if (view.ndim() != 2) @@ -310,7 +318,6 @@ NB_MODULE(pyalea_c, m) { } new (self) VecTs(rows); }) - .def(nb::init()) .def("timeseries", [](VecTs const & self) { return ts_to_numpy_vector_rows(self); }) .def_prop_ro("size", &VecTs::size) .def("__repr__", &stream_repr); diff --git a/bindings/python/pyalps/cpp/pymcdata.cpp b/bindings/python/pyalps/cpp/pymcdata.cpp index 9466d8d9f..aaaaa96b2 100644 --- a/bindings/python/pyalps/cpp/pymcdata.cpp +++ b/bindings/python/pyalps/cpp/pymcdata.cpp @@ -228,9 +228,15 @@ NB_MODULE(pymcdata_c, m) { nb::class_(m, "MCVectorData", "Vector-valued Monte Carlo data.") .def(nb::init<>()) - .def(nb::init>(), nb::arg("mean")) - .def(nb::init, std::vector>(), - nb::arg("mean"), nb::arg("error")) + .def("__init__", [](Vector * self, std::vector const & mean) { + new (self) Vector(mean, std::vector(mean.size(), 0.0)); + }, nb::arg("mean")) + .def("__init__", [](Vector * self, std::vector const & mean, + std::vector const & error) { + if (mean.size() != error.size()) + throw nb::value_error("mean and error must have the same length"); + new (self) Vector(mean, error); + }, nb::arg("mean"), nb::arg("error")) .def("__len__", [](Vector & v) { return v.mean().size(); diff --git a/test/pyalps/test_conversion_contracts.py b/test/pyalps/test_conversion_contracts.py new file mode 100644 index 000000000..bb55cd5a3 --- /dev/null +++ b/test/pyalps/test_conversion_contracts.py @@ -0,0 +1,94 @@ +"""Regression checks found by comparing the Boost.Python conversion contracts.""" + +import os +import subprocess +import sys +import textwrap + +import numpy as np +import pytest + +from pyalps import alea, ngs +from pyalps.cxx.pyngsaccumulator_c import count_accumulator + + +def test_count_accumulator_accepts_non_scalar_samples(): + accumulator = count_accumulator() + for sample in (np.ones((2, 3)), 1 + 2j, [1, 2], {"x": 1}, None): + accumulator(sample) + assert accumulator.count() == 5 + assert accumulator.result().count() == 5 + + +@pytest.mark.parametrize("vector", [False, True]) +def test_timeseries_from_mcdata(tmp_path, vector): + observable_type = alea.RealVectorTimeSeriesObservable if vector else alea.RealTimeSeriesObservable + data_type = alea.MCVectorData if vector else alea.MCScalarData + series_type = alea.MCVectorTimeseries if vector else alea.MCScalarTimeseries + observable = observable_type("samples") + for i in range(32): + observable << (np.array([float(i), 2.0 * i]) if vector else float(i)) + filename = str(tmp_path / "samples.h5") + observable.save(filename) + data = data_type() + data.load(filename, "/simulation/results/samples") + np.testing.assert_array_equal(series_type(data).timeseries(), data.bins) + + +@pytest.mark.parametrize("observable_type", [alea.RealVectorObservable, alea.RealVectorTimeSeriesObservable]) +def test_vector_convergence_is_an_integer_array(observable_type): + observable = observable_type("samples") + for i in range(128): + observable << np.array([float(i), 2.0 * i]) + convergence = observable.converged_errors + assert isinstance(convergence, np.ndarray) + assert convergence.shape == (2,) + assert convergence.dtype.kind == "i" + assert np.isin(convergence, [0, 1, 2]).all() + + +@pytest.mark.parametrize("layout", ["readonly", "strided", "fortran"]) +def test_array_consumers_do_not_require_writable_samples(layout): + values = np.arange(24.0).reshape(4, 6) + if layout == "readonly": + values.flags.writeable = False + elif layout == "strided": + values = values[:, ::2] + else: + values = np.asfortranarray(values) + np.testing.assert_array_equal(alea.MCScalarTimeseries(values[0]).timeseries(), values[0]) + np.testing.assert_array_equal(alea.MCVectorTimeseries(values).timeseries(), values) + observable = alea.RealVectorObservable("samples") + ngs_observable = ngs.createRealVectorObservable("samples") + for row in values: + observable << row + ngs_observable << row + np.testing.assert_allclose(observable.mean, values.mean(axis=0)) + np.testing.assert_allclose(ngs.observable2result(ngs_observable).mean, values.mean(axis=0)) + + +def test_mcvector_constructor_sizes_errors_before_indexing(): + # Printing/indexing a mean-only vector previously read an empty error + # vector and crashed the interpreter. Keep native crash checks isolated. + code = """ + import numpy as np + from pyalps.alea import MCVectorData + data = MCVectorData([1.0, 2.0, 3.0]) + np.testing.assert_array_equal(data.error, [0.0, 0.0, 0.0]) + assert data[1].mean == 2.0 + assert data[1].error == 0.0 + assert '2' in repr(data) + assert '2.00' in format(data, '.2f') + try: + MCVectorData([1.0, 2.0], [0.1]) + except ValueError: + pass + else: + raise AssertionError('mismatched mean/error lengths must be rejected') + """ + result = subprocess.run( + [sys.executable, "-X", "faulthandler", "-c", textwrap.dedent(code)], + capture_output=True, text=True, timeout=30, + env={**os.environ, "MallocScribble": "1"}, + ) + assert result.returncode == 0, result.stdout + result.stderr From 05f9fa4efc9e817b7e86903f67f160a51325fd6e Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Thu, 17 Sep 2026 23:11:55 -0400 Subject: [PATCH 41/52] Include utility before Boost Math 1.76 headers --- src/alps/numeric/special_functions.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/alps/numeric/special_functions.hpp b/src/alps/numeric/special_functions.hpp index 6c45e98ff..4bddc9d0a 100644 --- a/src/alps/numeric/special_functions.hpp +++ b/src/alps/numeric/special_functions.hpp @@ -19,6 +19,9 @@ #ifndef ALPS_NUMERIC_SPECIAL_FUNCTIONS_HPP #define ALPS_NUMERIC_SPECIAL_FUNCTIONS_HPP +// Boost.Math 1.76 includes inside its meta_programming namespace. +// Include it globally first so newer standard libraries are not declared there. +#include #include #include From 5a2bf118487b55447f8666e99dcf493944dca9f5 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 22:44:14 -0400 Subject: [PATCH 42/52] Support arbitrary-size MPI object receives --- .github/workflows/build_wheels.yml | 2 +- bindings/python/pyalps/src/pyalps/mpi.py | 179 +++++++++++++++++------ test/pyalps/test_mpi_requests.py | 113 ++++++++++++++ 3 files changed, 249 insertions(+), 45 deletions(-) create mode 100644 test/pyalps/test_mpi_requests.py diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 8d14f0336..0427eeb3a 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -168,7 +168,7 @@ jobs: python -m pip install --no-index --find-links wheelhouse pyalps - name: Run two-rank compatibility surface - run: mpiexec -n 2 python -m pytest -q test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface + run: mpiexec -n 2 python -m pytest -q test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface test/pyalps/test_mpi_requests.py upload_pypi: needs: [build_wheels, build_sdist, smoke_test, mpi_smoke] diff --git a/bindings/python/pyalps/src/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py index d8f1cfc10..0ca59ce06 100644 --- a/bindings/python/pyalps/src/pyalps/mpi.py +++ b/bindings/python/pyalps/src/pyalps/mpi.py @@ -17,6 +17,9 @@ import atexit as _atexit from functools import reduce as _python_reduce import sys +import threading as _threading +import time as _time +import weakref as _weakref from typing import Any try: @@ -53,39 +56,113 @@ Exception = _MPI.Exception Status = _MPI.Status +# Object-mode MPI.Irecv reserves a fixed-size pickle buffer (32 KiB by +# default). A matched probe gives us the actual message size instead. Keep +# probes in posting order, including across wrappers of the same communicator, +# so waiting for a later wildcard receive cannot steal an earlier one's data. +_pending_receives = [] +_receive_lock = _threading.RLock() + + +def _progress_receives(): + with _receive_lock: + pending = [] + for reference in _pending_receives: + request = reference() + if request is None or request._result is not None: + continue + status = Status() + message = request._comm.improbe(request._source, request._tag, status) + if message is None: + pending.append(reference) + else: + # A message can arrive between two probes. Give it to the + # earliest posted receive that matches its actual envelope. + recipient = request + for earlier in pending: + candidate = earlier() + if (candidate is not None and candidate._comm == request._comm + and candidate._source in (any_source, status.source) + and candidate._tag in (any_tag, status.tag)): + recipient = candidate + pending.remove(earlier) + pending.append(reference) + break + recipient._request = message.irecv() + _pending_receives[:] = pending + class Request: """Non-value request with the Boost.MPI ``wait``/``test`` contract.""" def __init__(self, request: Any): self._request = request + self._result = None + self._reported = False + + def _test(self): + if self._result is None and self._request is not None: + status = Status() + flag, value = self._request.test(status) + if flag: + self._result = value, status + return self._result + + def _wait(self): + while True: + # Progress receives even while waiting on a send: rendezvous + # sends (including self-sends) otherwise cannot complete. + _progress_receives() + result = self._test() + if result is not None: + self._reported = True + return result + _time.sleep(0.0001) def wait(self): - status = Status() - self._request.wait(status) - return status + return self._wait()[1] def test(self): - status = Status() - flag, _value = self._request.test(status) - return status if flag else None + _progress_receives() + result = self._test() + if result is not None: + self._reported = True + return result[1] + return None def cancel(self) -> None: - self._request.cancel() + with _receive_lock: + if self._result is not None: + return + if self._request is None: + status = Status() + status.Set_cancelled(True) + self._result = None, status + else: + self._request.cancel() class RequestWithValue(Request): """Receive request whose completion returns ``(value, status)``.""" + @classmethod + def _receive(cls, comm, source, tag): + request = cls(None) + request._comm, request._source, request._tag = comm, source, tag + with _receive_lock: + _pending_receives.append(_weakref.ref(request)) + _progress_receives() + return request + def wait(self): - status = Status() - value = self._request.wait(status) - return value, status + return self._wait() def test(self): - status = Status() - flag, value = self._request.test(status) - return (value, status) if flag else None + _progress_receives() + result = self._test() + if result is not None: + self._reported = True + return result class RequestList(list): @@ -115,7 +192,7 @@ def __eq__(self, other: object) -> bool: return isinstance(other, Communicator) and self._comm == other._comm def send(self, dest: int, tag: int = 0, value: Any = None) -> None: - self._comm.send(value, dest=dest, tag=tag) + self.isend(dest, tag, value).wait() def recv( self, @@ -123,15 +200,14 @@ def recv( tag: int = any_tag, return_status: bool = False, ) -> Any: - status = _MPI.Status() if return_status else None - value = self._comm.recv(source=source, tag=tag, status=status) + value, status = self.irecv(source, tag).wait() return (value, status) if return_status else value def isend(self, dest: int, tag: int = 0, value: Any = None): return Request(self._comm.isend(value, dest=dest, tag=tag)) def irecv(self, source: int = any_source, tag: int = any_tag): - return RequestWithValue(self._comm.irecv(source=source, tag=tag)) + return RequestWithValue._receive(self._comm, source, tag) def probe(self, source: int = any_source, tag: int = any_tag): status = _MPI.Status() @@ -216,58 +292,73 @@ def _check_requests(requests) -> None: raise TypeError("requests must contain pyalps.mpi Request objects") -def _raw_requests(requests): +def _poll_requests(requests): _check_requests(requests) - return [request._request for request in requests] + _progress_receives() + # Cache individual completions: test_all must not lose a value when only + # part of the batch has arrived, and every receive must get a chance to + # progress before a large send is waited on. + return [request._test() for request in requests] def wait_any(requests): - status = Status() - index, value = _MPI.Request.waitany(_raw_requests(requests), status) - return value, status, index + while True: + result = test_any(requests) + if result is not None: + return result + _time.sleep(0.0001) def test_any(requests): - status = Status() - index, flag, value = _MPI.Request.testany(_raw_requests(requests), status) - return (value, status, index) if flag else None + for index, result in enumerate(_poll_requests(requests)): + if result is not None and not requests[index]._reported: + requests[index]._reported = True + return result[0], result[1], index + if all(request._reported for request in requests): + return None, Status(), _MPI.UNDEFINED + return None def wait_all(requests, callable=None) -> None: - statuses = [Status() for _ in requests] - values = _MPI.Request.waitall(_raw_requests(requests), statuses) - if callable is not None: - for value, status in zip(values, statuses): - callable(value, status) + while not test_all(requests, callable): + _time.sleep(0.0001) def test_all(requests, callable=None) -> bool: - statuses = [Status() for _ in requests] - flag, values = _MPI.Request.testall(_raw_requests(requests), statuses) - if flag and callable is not None and values is not None: - for value, status in zip(values, statuses): + results = _poll_requests(requests) + if any(result is None for result in results): + return False + for request in requests: + request._reported = True + if callable is not None: + for value, status in results: callable(value, status) - return bool(flag) + return True def wait_some(requests, callable=None) -> int: - statuses = [Status() for _ in requests] - indices, values = _MPI.Request.waitsome(_raw_requests(requests), statuses) - return _finish_some(requests, indices, values, statuses, callable) + while True: + boundary = test_some(requests, callable) + if boundary < len(requests) or all(r._reported for r in requests): + return boundary + _time.sleep(0.0001) def test_some(requests, callable=None) -> int: - statuses = [Status() for _ in requests] - indices, values = _MPI.Request.testsome(_raw_requests(requests), statuses) - return _finish_some(requests, indices, values, statuses, callable) + results = _poll_requests(requests) + indices = [i for i, result in enumerate(results) + if result is not None and not requests[i]._reported] + for index in indices: + requests[index]._reported = True + return _finish_some(requests, indices, results, callable) -def _finish_some(requests, indices, values, statuses, callable) -> int: +def _finish_some(requests, indices, results, callable) -> int: if not indices: return len(requests) if callable is not None: - for value, status in zip(values, statuses): - callable(value, status) + for index in indices: + callable(*results[index]) # Boost.MPI partitions the mutable RequestList into pending requests # followed by completed requests and returns the first completed index. diff --git a/test/pyalps/test_mpi_requests.py b/test/pyalps/test_mpi_requests.py new file mode 100644 index 000000000..d03b7419a --- /dev/null +++ b/test/pyalps/test_mpi_requests.py @@ -0,0 +1,113 @@ +"""Exercise real object transport; run both normally and under two-rank MPI.""" + +import time + +import numpy as np +import pytest + +pytest.importorskip("mpi4py") +from pyalps import mpi + + +def poll(function): + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + result = function() + if result is not None and result is not False: + return result + time.sleep(0.0001) + pytest.fail("MPI request did not complete") + + +@pytest.mark.parametrize("completion", ["wait", "test", "wait_all", "test_all", "wait_any", "test_any", "wait_some", "test_some"]) +def test_large_object_requests(completion): + destination = (mpi.rank + 1) % mpi.size + source = (mpi.rank - 1) % mpi.size + payload = {"source": mpi.rank, "data": b"x" * (2 * 1024 * 1024)} + send = mpi.world.isend(destination, 810, payload) + receive = mpi.world.irecv(source, 810) + callbacks = [] + if completion == "wait": + value, status = receive.wait() + elif completion == "test": + value, status = poll(receive.test) + elif completion.endswith("any"): + value, status, index = poll(lambda: getattr(mpi, completion)([receive])) + assert index == 0 + else: + # Put the rendezvous send first, so a sequential blocking wait would + # deadlock unless the receive is progressed before the send completes. + requests = mpi.RequestList([send, receive]) + callback = lambda value, status: callbacks.append((value, status)) + if completion == "wait_all": + mpi.wait_all(requests, callback) + elif completion == "test_all": + poll(lambda: mpi.test_all(requests, callback)) + else: + deadline = time.monotonic() + 15 + while requests and time.monotonic() < deadline: + boundary = getattr(mpi, completion)(requests, callback) + del requests[boundary:] + assert not requests + value, status = next(pair for pair in callbacks if pair[0] is not None) + assert len(callbacks) == 2 + send.wait() + assert value == {"source": source, "data": payload["data"]} + assert status.source == source and status.tag == 810 + mpi.world.barrier() + + +def test_posted_receives_keep_order_when_waited_backwards(): + # Two wrappers of the same communicator must share posting order. + first = mpi.world.irecv(mpi.any_source, mpi.any_tag) + second = mpi.Communicator(mpi.world).irecv(mpi.any_source, mpi.any_tag) + sends = [mpi.world.isend(mpi.rank, 811, i) for i in (1, 2)] + assert second.wait()[0] == 2 + assert first.wait()[0] == 1 + mpi.wait_all(sends) + mpi.world.barrier() + + +def test_partial_test_all_keeps_received_values(): + requests = [mpi.world.irecv(mpi.rank, tag) for tag in (812, 813)] + first_send = mpi.world.isend(mpi.rank, 812, "first") + assert poll(requests[0].test)[0] == "first" + callbacks = [] + assert not mpi.test_all(requests, lambda *pair: callbacks.append(pair)) + assert callbacks == [] + second_send = mpi.world.isend(mpi.rank, 813, "second") + mpi.wait_all(requests, lambda *pair: callbacks.append(pair)) + mpi.wait_all([first_send, second_send]) + assert [value for value, status in callbacks] == ["first", "second"] + assert [status.tag for value, status in callbacks] == [812, 813] + mpi.world.barrier() + + +def test_cancelling_unmatched_receive_does_not_consume_later_message(): + receive = mpi.world.irecv(mpi.rank, 814) + assert receive.test() is None + receive.cancel() + value, status = receive.wait() + assert value is None and status.Is_cancelled() + send = mpi.world.isend(mpi.rank, 814, "after cancellation") + assert mpi.world.recv(mpi.rank, 814) == "after cancellation" + send.wait() + mpi.world.barrier() + + +def test_blocking_large_send_progresses_posted_receive(): + receive = mpi.world.irecv(mpi.rank, 815) + mpi.world.send(mpi.rank, 815, b"y" * (2 * 1024 * 1024)) + assert receive.wait()[0] == b"y" * (2 * 1024 * 1024) + mpi.world.barrier() + + +def test_large_numpy_payload_with_wildcard_receive(): + values = (np.arange(256 * 1024).reshape(512, 512) + mpi.rank * 1j).astype(np.complex128) + source = (mpi.rank - 1) % mpi.size + send = mpi.world.isend((mpi.rank + 1) % mpi.size, 816, {"array": values, "source": mpi.rank}) + value, status = mpi.world.irecv(mpi.any_source, 816).wait() + send.wait() + assert status.source == source and value["source"] == source + np.testing.assert_array_equal(value["array"], np.arange(256 * 1024).reshape(512, 512) + source * 1j) + mpi.world.barrier() From c1fb274aeb538c5b5cb5200f7f379471eac9c4a8 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 22:44:17 -0400 Subject: [PATCH 43/52] Preserve HDF5 value types and archive contexts --- .../pyalps/cpp/ngs/extract_from_pyobject.hpp | 16 ++++- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 26 +++++++- src/alps/hdf5/archive.cpp | 41 ++++++++++++- test/pyalps/test_archive_dtypes.py | 60 +++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 test/pyalps/test_archive_dtypes.py diff --git a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp index 6619cbd85..c84fa9a02 100644 --- a/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp +++ b/bindings/python/pyalps/cpp/ngs/extract_from_pyobject.hpp @@ -18,6 +18,7 @@ #include #include #include + #include #include #include #include @@ -48,7 +49,20 @@ template void extract_from_pyobject_py11(T & visitor, nb_::handle data) { std::string dtype = data.ptr()->ob_type->tp_name; if (dtype == "bool") visitor(nb_::cast(data)); - else if (dtype == "int") visitor(nb_::cast(data)); + else if (dtype == "int") { + int overflow = 0; + long long value = PyLong_AsLongLongAndOverflow(data.ptr(), &overflow); + if (PyErr_Occurred()) throw nb_::python_error(); + if (overflow) { + unsigned long long wide = PyLong_AsUnsignedLongLong(data.ptr()); + if (PyErr_Occurred()) throw nb_::python_error(); + visitor(wide); + } else if (value >= std::numeric_limits::min() + && value <= std::numeric_limits::max()) + visitor(static_cast(value)); + else + visitor(value); + } else if (dtype == "long") visitor(nb_::cast(data)); else if (dtype == "float") visitor(nb_::cast(data)); else if (dtype == "complex") visitor(nb_::cast>(data)); diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index caa9603a7..4ca38b1fe 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -427,8 +427,9 @@ namespace alps { return std::strcmp(Py_TYPE(attr.ptr())->tp_name, "method") == 0; } void python_hdf5_save(alps::hdf5::archive & ar, - std::string const & path, + std::string const & relative_path, nb::handle data) { + std::string const path = ar.complete_path(relative_path); if (has_archive_save_method(data)) { save_in_path_context(ar, path, [&] { nb::getattr(data, "save")(nb::cast(&ar, nb::rv_policy::reference)); @@ -483,7 +484,7 @@ namespace alps { std::string const & path); nb::object python_hdf5_load(alps::hdf5::archive & ar, std::string const & path) { - return python_hdf5_load_impl(ar, path); + return python_hdf5_load_impl(ar, ar.complete_path(path)); } nb::object python_hdf5_load_impl(alps::hdf5::archive & ar, std::string const & path) { @@ -571,6 +572,27 @@ namespace alps { // Windows — so we can't rely on just `int` matching). #define TRY_SCALAR(T) \ if (ar.is_datatype(path)) { T v; ar[path] >> v; return nb::cast(v); } + if (ar.is_datatype(path)) { + std::string marker = ar.complete_path(path); + auto at = marker.find_last_of('@'); + marker = at == std::string::npos + ? marker + "/@__alps_type__" + : marker.substr(0, at) + "@__alps_type__:" + marker.substr(at + 1); + std::string kind; + if (ar.is_attribute(marker)) + ar[marker] >> kind; + // Unmarked legacy signed-byte data was read as bool by the + // Boost.Python loader. New native/Python writes distinguish + // int8 explicitly, including scalar and attribute values. + if (ar.is_scalar(path)) { + if (kind == "int8") { + std::int8_t value; ar[path] >> value; return nb::cast(value); + } + bool value; ar[path] >> value; return nb::cast(value); + } + auto array = load_nd_array(ar, path, ar.extent(path)); + return kind == "int8" ? array : array.attr("astype")("bool"); + } if (ar.is_scalar(path)) { TRY_SCALAR(std::string) TRY_SCALAR(double) diff --git a/src/alps/hdf5/archive.cpp b/src/alps/hdf5/archive.cpp index 9340397ac..78158f95b 100644 --- a/src/alps/hdf5/archive.cpp +++ b/src/alps/hdf5/archive.cpp @@ -187,6 +187,30 @@ namespace alps { return type_id; } + // bool and signed char historically share H5T_NATIVE_SCHAR. + // Mark new writes without changing the on-disk numeric type, so + // old ALPS readers and native typed reads remain compatible. + inline void mark_signed_byte(archive const & ar, std::string path, + std::string const & kind) { + std::size_t at = path.find_last_of('@'); + if (at != std::string::npos) { + std::string name = path.substr(at + 1); + if (name.compare(0, 2, "__") == 0) + return; + path = path.substr(0, at) + "@__alps_type__:" + name; + } else + path += "/@__alps_type__"; + ar.write(path, kind); + } + template + void mark_signed_byte(archive const &, std::string const &, T *) {} + inline void mark_signed_byte(archive const & ar, std::string const & path, bool *) { + mark_signed_byte(ar, path, std::string("bool")); + } + inline void mark_signed_byte(archive const & ar, std::string const & path, signed char *) { + mark_signed_byte(ar, path, std::string("int8")); + } + hid_t open_attribute(archive const & ar, hid_t file_id, std::string path) { if ((path = ar.complete_path(path)).find_last_of('@') == std::string::npos) throw invalid_path("no attribute path: " + path + ALPS_STACKTRACE); @@ -489,6 +513,7 @@ namespace alps { ctx = ctx.substr(0, ctx.find_last_of('/')); path = path.size() == 2 ? "" : path.substr(3); } + if (ctx.empty()) ctx = "/"; return ctx + (ctx.size() == 1 || !path.size() ? "" : "/") + path; } } @@ -716,8 +741,14 @@ namespace alps { throw archive_closed("the archive is closed" + ALPS_STACKTRACE); if ((path = complete_path(path)).find_last_of('@') == std::string::npos) throw invalid_path("no attribute path: " + path + ALPS_STACKTRACE); - // TODO: implement - throw std::logic_error("Not implemented!" + ALPS_STACKTRACE); + ALPS_HDF5_FAKE_THREADSAFETY + if (is_attribute(path)) { + auto at = path.find_last_of('@'); + std::string parent = path.substr(0, at - 1); + if (parent.empty()) parent = "/"; + detail::check_error(H5Adelete_by_name(context_->file_id_, + parent.c_str(), path.substr(at + 1).c_str(), H5P_DEFAULT)); + } } void archive::set_complex(std::string path) { @@ -1032,6 +1063,7 @@ namespace alps { else \ detail::check_data(parent_id); \ } \ + detail::mark_signed_byte(*this, path, static_cast(nullptr)); \ } ALPS_NGS_FOREACH_NATIVE_HDF5_TYPE(ALPS_NGS_HDF5_WRITE_SCALAR) #undef ALPS_NGS_HDF5_WRITE_SCALAR @@ -1220,6 +1252,7 @@ namespace alps { else \ detail::check_data(parent_id); \ } \ + detail::mark_signed_byte(*this, path, static_cast(nullptr)); \ } ALPS_NGS_FOREACH_NATIVE_HDF5_TYPE(ALPS_NGS_HDF5_WRITE_VECTOR) #undef ALPS_NGS_HDF5_WRITE_VECTOR @@ -1312,6 +1345,10 @@ namespace alps { #undef ALPS_NGS_HDF5_IS_DATATYPE_IMPL_IMPL void archive::construct(std::string const & filename, std::size_t props) { + // Contexts must be absolute so restoring the root after nested + // save/load calls cannot resolve an empty string relative to the + // last dataset visited. + current_ = "/"; ALPS_HDF5_LOCK_MUTEX detail::check_error(H5Eset_auto2(H5E_DEFAULT, NULL, NULL)); if (props & COMPRESS) { diff --git a/test/pyalps/test_archive_dtypes.py b/test/pyalps/test_archive_dtypes.py new file mode 100644 index 000000000..129b02073 --- /dev/null +++ b/test/pyalps/test_archive_dtypes.py @@ -0,0 +1,60 @@ +"""Check dtype-dependent user operations, not just equal numeric values.""" + +import numpy as np +import pytest + +from pyalps import hdf5, ngs + + +@pytest.mark.parametrize("dtype", [np.bool_, np.int8]) +@pytest.mark.parametrize("shape", [(), (3,), (2, 3), (2, 1, 3), (0,), (2, 0)]) +@pytest.mark.parametrize("attribute", [False, True]) +def test_boolean_and_signed_byte_round_trip(tmp_path, dtype, shape, attribute): + size = int(np.prod(shape)) + value = np.arange(size, dtype=np.int8).reshape(shape).astype(dtype) + if value.ndim > 1: + value = np.asfortranarray(value) + value.flags.writeable = False + path = "/group/@value" if attribute else "/value" + filename = str(tmp_path / "dtype.h5") + with hdf5.archive(filename, "w") as archive: + archive.create_group("/group") + archive[path] = value + with hdf5.archive(filename, "r") as archive: + actual = archive[path] + if shape: + assert actual.dtype == dtype + assert actual.shape == shape + else: + assert type(actual) is (bool if dtype == np.bool_ else int) + np.testing.assert_array_equal(actual, value) + + +def test_boolean_mask_remains_a_mask_after_reload(tmp_path): + with hdf5.archive(str(tmp_path / "mask.h5"), "w") as archive: + archive["mask"] = np.array([True, False, True]) + np.testing.assert_array_equal(np.array([10, 20, 30])[archive["mask"]], [10, 30]) + # Cover the native vector writer as well as ndarray dispatch. + archive["parameters"] = ngs.params({"mask": [True, False, True]}) + np.testing.assert_array_equal( + np.array([10, 20, 30])[archive["parameters/mask"]], [10, 30] + ) + + +@pytest.mark.parametrize("attribute", [False, True]) +def test_overwriting_same_storage_type_updates_dtype_marker(tmp_path, attribute): + path = "/group/@value" if attribute else "/value" + with hdf5.archive(str(tmp_path / "overwrite.h5"), "w") as archive: + archive.create_group("/group") + for dtype in (np.bool_, np.int8, np.bool_, np.int8): + archive[path] = np.array([0, 1], dtype=dtype) + assert archive[path].dtype == dtype + + +def test_unmarked_legacy_boolean_dataset(tmp_path): + with hdf5.archive(str(tmp_path / "legacy.h5"), "w") as archive: + archive["mask"] = np.array([True, False, True]) + archive.delete_attribute("mask/@__alps_type__") + mask = archive["mask"] + assert mask.dtype == np.bool_ + np.testing.assert_array_equal(np.arange(3)[mask], [0, 2]) From 14e63158d19895193e2f36d0f82dcc24029c0e4c Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 22:44:20 -0400 Subject: [PATCH 44/52] Preserve mutable Python parameters across native calls and checkpoints --- bindings/python/pyalps/README.md | 28 ++++ bindings/python/pyalps/cpp/dict_to_params.hpp | 110 +++++++++++-- bindings/python/pyalps/cpp/ngs/params.cpp | 64 ++++++-- src/alps/ngs/cast.hpp | 29 ++++ src/alps/ngs/detail/paramvalue.hpp | 39 ++++- src/alps/ngs/lib/params.cpp | 14 +- src/alps/ngs/lib/paramvalue.cpp | 15 +- src/alps/ngs/params.hpp | 8 + test/ngs/params/CMakeLists.txt | 6 +- test/ngs/params/external.cpp | 54 +++++++ test/pyalps/native_params/CMakeLists.txt | 17 +++ test/pyalps/native_params/check.py | 64 ++++++++ test/pyalps/native_params/probe.cpp | 44 ++++++ test/pyalps/test_binding_surface.py | 144 +++++------------- tutorials/ngs/5_export_python/smoke_test.py | 10 +- 15 files changed, 502 insertions(+), 144 deletions(-) create mode 100644 test/ngs/params/external.cpp create mode 100644 test/pyalps/native_params/CMakeLists.txt create mode 100644 test/pyalps/native_params/check.py create mode 100644 test/pyalps/native_params/probe.cpp diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 9febf50d9..b2f72b4fe 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -66,6 +66,34 @@ into neither of nanobind's special ABI modes: every such consumer to use the limited API too. Per-version wheels preserve ordinary downstream extension interoperability. +## Compatibility and checkpoints + +Parameters created from Python retain their Python values. NumPy arrays keep +array arithmetic, and changes through a list, array, or shared reference are +visible to subsequent Python and C++ reads. A C++ consumer converts the current +value to its requested scalar or one-dimensional vector type; incompatible +metadata and out-of-range conversions raise an exception. Python metadata may +use other shapes and containers supported by the HDF5 writer. Objects such as +`None` can be held in memory but have no ALPS HDF5 representation. + +The C++ SDK remains independent of Python and nanobind. Python-owned values and +their checkpoint decoder are supplied by the bindings. Rebuild downstream C++ +extensions against the SDK from the same source revision as the wheel; the +parameter layout changed during this migration. + +New HDF5 writes distinguish Boolean and signed-byte values with an +`__alps_type__` attribute while retaining the existing numeric storage format. +Unmarked signed-byte data from old ALPS files retains the legacy Boolean +interpretation. The old format cannot distinguish an unmarked `int8` array +from a Boolean mask; use a typed reader such as h5py when an old dataset is +known to contain signed bytes. + +`pyalps.mpi` receives Python objects using matched probes, so asynchronous +receives and the wait/test helpers can handle messages larger than mpi4py's +default object receive buffer. This adapter exchanges mpi4py messages; +Boost.MPI's C++ serialization protocol and skeleton/content API are not wire +compatible. Communicating processes must use the same protocol. + ## Versioning pyalps does not carry a version of its own. The numeric version is read from diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index db86a7d28..6f6688116 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -147,14 +147,14 @@ inline std::string string_value(nb::handle value) { return nb::cast(decoded); } } // namespace detail -// Store one Python value under `key`. paramvalue's only integral +// Materialize a checked native snapshot. paramvalue's only integral // alternative is a 32-bit int and libalps static_casts wider integer // types down to it, so out-of-range integers are rejected loudly here // — for scalars and inside lists alike — rather than truncated or // silently widened to double. Sequence elements are classified before // conversion so integers round-trip as ints, mixed real numerics widen to // double, and complex values can never be coerced through a real-number path. -inline void set_param_value(alps::params & p, std::string const & key, nb::handle value) { +inline void set_native_param_value(alps::params & p, std::string const & key, nb::handle value) { if (value.is_none()) throw nb::type_error(("cannot store None for parameter '" + key + "': params has no null type; delete the key instead").c_str()); @@ -167,14 +167,13 @@ inline void set_param_value(alps::params & p, std::string const & key, nb::handl throw nb::python_error(); p[key] = (truth == 1); } else if (detail::is_numpy_array(value)) { - // params is deliberately Python-object-free. Convert the NumPy - // value once at the boundary and store one of paramvalue's native - // scalar/vector alternatives. ALPS parameters are one-dimensional; - // preserving an arbitrary N-D ndarray would require an object escape - // hatch or a new tensor type in the C++ API. + // A native consumer can request scalar/vector values. The original + // Python object (including higher-rank metadata) remains owned by + // python_paramvalue_source; only this snapshot has the native shape + // restrictions. std::size_t const ndim = nb::cast(value.attr("ndim")); if (ndim == 0) { - set_param_value(p, key, value.attr("item")()); + set_native_param_value(p, key, value.attr("item")()); return; } if (ndim != 1) @@ -199,7 +198,7 @@ inline void set_param_value(alps::params & p, std::string const & key, nb::handl + kind + "'").c_str()); return; } - set_param_value(p, key, items); + set_native_param_value(p, key, items); return; } else if (scalar_type == detail::scalar_kind::integer) { p[key] = detail::integer_value(value, key); @@ -290,8 +289,101 @@ inline void set_param_value(alps::params & p, std::string const & key, nb::handl " numpy array, or a sequence of those scalar types)").c_str()); } } +// Keep interpreter ownership in the binding, not in libalps. Copies share the +// provider (matching the original object-valued parameters), native consumers +// request a checked snapshot, and Python lookups/saves see all mutations. +class python_paramvalue_source final : public alps::detail::paramvalue_source { +public: + python_paramvalue_source(nb::handle value, std::string key) + : value_(value.inc_ref().ptr()), key_(std::move(key)) {} + ~python_paramvalue_source() override { + if (Py_IsInitialized()) { + nb::gil_scoped_acquire gil; + Py_DECREF(value_); + } + } + alps::detail::paramvalue native_value() const override { + nb::gil_scoped_acquire gil; + // Defer large integers to ALPS' checked text-to-target conversion. + // Narrowing to the native variant's int first would corrupt values. + nb::object normalized = nb::borrow(value_); + if (detail::is_numpy_array(normalized) + && nb::cast(normalized.attr("ndim")) == 0) + normalized = normalized.attr("item")(); + nb::handle value(normalized); + if (!detail::is_numpy_array(value) + && detail::classify_scalar(value) == detail::scalar_kind::integer) { + nb::object integer = nb::steal(PyNumber_Index(value.ptr())); + if (!integer.is_valid()) throw nb::python_error(); + int overflow = 0; + long long number = PyLong_AsLongLongAndOverflow(integer.ptr(), &overflow); + if (PyErr_Occurred()) throw nb::python_error(); + if (overflow || number < std::numeric_limits::min() + || number > std::numeric_limits::max()) + return alps::detail::paramvalue(nb::cast(nb::str(integer))); + } + nb::object items = nb::borrow(value); + if (detail::is_numpy_array(value) && nb::cast(value.attr("ndim")) == 1) + items = value.attr("tolist")(); + if (nb::isinstance(items) || nb::isinstance(items)) { + bool integral = nb::len(items) > 0, wide = false; + std::vector integers; + for (std::size_t i = 0; i < nb::len(items); ++i) { + nb::object item = items[i]; + if (detail::classify_scalar(item) != detail::scalar_kind::integer) { + integral = false; + break; + } + nb::object integer = nb::steal(PyNumber_Index(item.ptr())); + if (!integer.is_valid()) throw nb::python_error(); + int overflow = 0; + long long number = PyLong_AsLongLongAndOverflow(integer.ptr(), &overflow); + if (PyErr_Occurred()) throw nb::python_error(); + wide |= overflow || number < std::numeric_limits::min() + || number > std::numeric_limits::max(); + integers.push_back(nb::cast(nb::str(integer))); + } + if (integral && wide) + return alps::detail::paramvalue(integers); + } + alps::params snapshot; + set_native_param_value(snapshot, key_, value); + return *snapshot.find(key_); + } + void save(alps::hdf5::archive & ar) const override { + nb::gil_scoped_acquire gil; + nb::module_::import_("pyalps.cxx.pyngshdf5_c"); + nb::cast(&ar, nb::rv_policy::reference).attr("__setitem__")("", nb::handle(value_)); + } + void print(std::ostream & stream) const override { + nb::gil_scoped_acquire gil; + stream << nb::cast(nb::str(nb::handle(value_))); + } + void * object(char const * binding) const override { + return std::strcmp(binding, "python") == 0 ? value_ : nullptr; + } +private: + PyObject * value_; + std::string key_; +}; + +inline void set_param_value(alps::params & p, std::string const & key, nb::handle value) { + p[key] = alps::detail::paramvalue(std::make_shared(value, key)); +} + +inline void enable_python_param_reader(alps::params & params) { + params.set_value_reader([](alps::hdf5::archive & ar) { + nb::gil_scoped_acquire gil; + nb::module_::import_("pyalps.cxx.pyngshdf5_c"); + nb::object value = nb::cast(&ar, nb::rv_policy::reference).attr("__getitem__")(""); + return alps::detail::paramvalue( + std::make_shared(value, ar.get_context())); + }); +} + inline alps::params params_from_dict(nb::dict const & values) { alps::params result; + enable_python_param_reader(result); for (auto item : values) set_param_value(result, nb::cast(nb::str(item.first)), diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index 0703aaf31..abf98bd89 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -29,23 +29,44 @@ struct paramvalue_to_py_visitor : boost::static_visitor { nb::object operator()(T const & value) const { return nb::cast(value); } + template + nb::object operator()(std::vector const & value) const { + // An empty sequence has no elements from which NumPy can infer its + // type. Preserve the native family (especially Boolean masks). + if constexpr (std::is_same::value) + return alps::python::numpy_module().attr("array")( + nb::cast(value), nb::arg("dtype") = "str"); + else + return alps::python::numpy_module().attr("array")( + nb::cast(value), nb::arg("dtype") = alps::python::numpy_dtype::name); + } }; nb::object paramvalue_to_py(alps::detail::paramvalue const & pv) { + if (pv.source()) { + auto * value = static_cast(pv.source()->object("python")); + if (value) + return nb::borrow(value); + return paramvalue_to_py(pv.source()->native_value()); + } return boost::apply_visitor( paramvalue_to_py_visitor(), static_cast(pv)); } -// Deposit a native C++ value from a Python object into the paramvalue -// via paramproxy's templated operator= — shared ladder in -// ../dict_to_params.hpp so params, mcbase and the application modules -// all ingest values identically. +// Retain the value through the shared binding-owned provider. void params_setitem(alps::params & self, nb::object const & key_obj, nb::handle value) { pyalps::set_param_value(self, nb::cast(nb::str(key_obj)), value); } nb::object params_getitem(alps::params & self, nb::object const & key_obj) { std::string key = nb::cast(nb::str(key_obj)); alps::detail::paramvalue const * value = self.find(key); - return value ? paramvalue_to_py(*value) : nb::none(); + if (!value) + return nb::none(); + nb::object result = paramvalue_to_py(*value); + // Materialize native checkpoint values once. Retain the object so later + // mutations survive both subsequent Python lookups and C++ conversions. + if (!value->source()) + pyalps::set_param_value(self, key, result); + return result; } void params_delitem(alps::params & self, nb::object const & key_obj) { self.erase(nb::cast(nb::str(key_obj))); @@ -57,24 +78,32 @@ nb::object value_or_default(alps::params & self, nb::object const & key, nb::obj return params_contains(self, key) ? params_getitem(self, key) : dflt; } void params_load(alps::params & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); + alps::hdf5::archive reader(ar); + reader.set_context(ar.complete_path(path)); + pyalps::enable_python_param_reader(self); + self.load(reader); } std::string params_print(alps::params & self) { std::stringstream ss; ss << self; return ss.str(); } -// deepcopy support — nanobind passes (self, memo); memo unused. -alps::params params_deepcopy(alps::params const & self, nb::handle /*memo*/) { - return alps::params(self); +// deepcopy support, including shared objects in the caller's memo. +alps::params params_deepcopy(alps::params & self, nb::handle memo) { + nb::dict values; + for (auto const & entry : self) { + nb::str key(entry.first.c_str()); + values[key] = params_getitem(self, key); + } + return pyalps::params_from_dict(nb::cast( + nb::module_::import_("copy").attr("deepcopy")(values, memo))); } } // namespace NB_MODULE(pyngsparams_c, m) { nb::class_(m, "params") - .def(nb::init<>()) + .def("__init__", [](alps::params * self) { + new (self) alps::params(pyalps::params_from_dict(nb::dict())); + }) .def("__init__", [](alps::params * self, nb::dict const & d) { new (self) alps::params(pyalps::params_from_dict(d)); @@ -85,15 +114,20 @@ NB_MODULE(pyngsparams_c, m) { .def("__init__", [](alps::params * self, std::string const & filename) { new (self) alps::params(boost::filesystem::path(filename)); + pyalps::enable_python_param_reader(*self); }, nb::arg("filename")) - .def(nb::init(), + .def("__init__", [](alps::params * self, alps::hdf5::archive & ar, std::string const & path) { + alps::params loaded; + params_load(loaded, ar, path); + new (self) alps::params(loaded); + }, nb::arg("archive"), nb::arg("path") = std::string("/parameters")) .def("__len__", [](alps::params const & self) { return self.size(); }) .def("__deepcopy__", ¶ms_deepcopy) .def("__getitem__", ¶ms_getitem) - .def("__setitem__", ¶ms_setitem) + .def("__setitem__", ¶ms_setitem, nb::arg("key"), nb::arg("value").none()) .def("__delitem__", ¶ms_delitem) .def("__contains__", ¶ms_contains) .def("__iter__", [](alps::params & self) { diff --git a/src/alps/ngs/cast.hpp b/src/alps/ngs/cast.hpp index 287fd5d7f..0cb44e176 100644 --- a/src/alps/ngs/cast.hpp +++ b/src/alps/ngs/cast.hpp @@ -14,6 +14,11 @@ #ifndef ALPS_NGS_CAST_HPP #define ALPS_NGS_CAST_HPP +#include +#include +#include +#include + #include #include @@ -73,6 +78,28 @@ namespace alps { } }; + namespace detail { + template T checked_integer_string(std::string const & text) { + // Keep the historical decimal-prefix parsing (e.g. "4.0" -> 4), + // but never let scanf silently wrap an out-of-range parameter. + errno = 0; + if constexpr (std::is_signed::value) { + long long value = std::strtoll(text.c_str(), nullptr, 10); + if (errno == ERANGE || value < std::numeric_limits::min() + || value > std::numeric_limits::max()) + throw std::out_of_range("integer parameter out of range: " + text); + return static_cast(value); + } else { + auto first = text.find_first_not_of(" \t\r\n\f\v"); + unsigned long long value = std::strtoull(text.c_str(), nullptr, 10); + if (errno == ERANGE || value > std::numeric_limits::max() + || (first != std::string::npos && text[first] == '-')) + throw std::out_of_range("integer parameter out of range: " + text); + return static_cast(value); + } + } + } + #define ALPS_NGS_CAST_STRING(T, p, c) \ template<> struct cast_hook { \ static inline std::string apply( T arg) { \ @@ -86,6 +113,8 @@ namespace alps { }; \ template<> struct cast_hook< T, std::string> { \ static inline T apply(std::string arg) { \ + if constexpr (std::is_integral::value) \ + return detail::checked_integer_string(arg); \ T value = 0; \ if (arg.size() && sscanf(arg.c_str(), "%" c, &value) < 0) \ throw std::runtime_error( \ diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index c17b95400..4ca192118 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include #define ALPS_NGS_FOREACH_PARAMETERVALUE_ADDABLE_TYPE(CALLBACK) \ CALLBACK(double) \ @@ -89,6 +91,18 @@ namespace alps { class paramvalue; + // Optional value provider for language bindings. No interpreter + // headers or runtime are required by the native library. A binding + // can retain a mutable value and supply a checked native snapshot + // only when a C++ consumer actually requests it. + struct ALPS_DECL paramvalue_source { + virtual ~paramvalue_source(); + virtual paramvalue native_value() const = 0; + virtual void save(hdf5::archive &) const = 0; + virtual void print(std::ostream &) const = 0; + virtual void * object(char const * binding) const = 0; + }; + } template T extract (detail::paramvalue const & arg); @@ -134,14 +148,23 @@ namespace alps { paramvalue(paramvalue const & v) : paramvalue_base(static_cast(v)) + , source_(v.source_) {} + explicit paramvalue(std::shared_ptr source) + : source_(std::move(source)) {} + + std::shared_ptr const & source() const { return source_; } + paramvalue const& operator=(paramvalue const& x) { static_cast(*this) = static_cast(x); + source_ = x.source_; return *this; } template T cast() const { + if (source_) + return source_->native_value().cast(); paramvalue_reader< T > visitor; boost::apply_visitor(visitor, *this); return visitor.get_value(); @@ -189,6 +212,8 @@ namespace alps { void load(hdf5::archive &); private: + + std::shared_ptr source_; friend class boost::serialization::access; @@ -196,7 +221,11 @@ namespace alps { Archive & ar, const unsigned int version ) const { paramvalue_serializer visitor(ar); - boost::apply_visitor(visitor, *this); + if (source_) { + paramvalue native = source_->native_value(); + boost::apply_visitor(visitor, native); + } else + boost::apply_visitor(visitor, *this); } template void load( @@ -225,9 +254,7 @@ namespace alps { ALPS_DECL std::ostream & operator<<(std::ostream & os, paramvalue const & arg); template T extract_impl (paramvalue const & arg, T) { - paramvalue_reader< T > visitor; - boost::apply_visitor(visitor, arg); - return visitor.get_value(); + return arg.cast(); } } @@ -238,9 +265,7 @@ namespace alps { }; template T extract (detail::paramvalue const & arg) { - detail::paramvalue_reader< T > visitor; - boost::apply_visitor(visitor, arg); - return visitor.get_value(); + return arg.cast(); } } diff --git a/src/alps/ngs/lib/params.cpp b/src/alps/ngs/lib/params.cpp index 3ced4fe23..6c78b4a5c 100644 --- a/src/alps/ngs/lib/params.cpp +++ b/src/alps/ngs/lib/params.cpp @@ -96,14 +96,20 @@ namespace alps { } void params::load(hdf5::archive & ar) { - keys.clear(); - values.clear(); + params loaded; std::vector list = ar.list_children(ar.get_context()); for (std::vector::const_iterator it = list.begin(); it != list.end(); ++it) { detail::paramvalue value; - ar[*it] >> value; - setter(*it, value); + if (value_reader_) { + hdf5::archive reader(ar); + reader.set_context(ar.complete_path(*it)); + value = value_reader_(reader); + } else + ar[*it] >> value; + loaded.setter(*it, value); } + keys.swap(loaded.keys); + values.swap(loaded.values); } #ifdef ALPS_HAVE_MPI diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index 27776bbf3..447188910 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -21,6 +21,8 @@ namespace alps { namespace detail { + paramvalue_source::~paramvalue_source() = default; + struct paramvalue_saver: public boost::static_visitor<> { paramvalue_saver(hdf5::archive & a) @@ -50,9 +52,7 @@ namespace alps { #define ALPS_NGS_PARAMVALUE_OPERATOR_T_IMPL(T) \ paramvalue::operator T () const { \ - paramvalue_reader< T > visitor; \ - boost::apply_visitor(visitor, *this); \ - return visitor.get_value(); \ + return cast(); \ } ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(ALPS_NGS_PARAMVALUE_OPERATOR_T_IMPL) #undef ALPS_NGS_PARAMVALUE_OPERATOR_T_IMPL @@ -60,12 +60,17 @@ namespace alps { #define ALPS_NGS_PARAMVALUE_OPERATOR_EQ_IMPL(T) \ paramvalue & paramvalue::operator=( T const & arg) { \ paramvalue_base::operator=(arg); \ + source_.reset(); \ return *this; \ } ALPS_NGS_FOREACH_PARAMETERVALUE_TYPE(ALPS_NGS_PARAMVALUE_OPERATOR_EQ_IMPL) #undef ALPS_NGS_PARAMVALUE_OPERATOR_EQ_IMPL void paramvalue::save(hdf5::archive & ar) const { + if (source_) { + source_->save(ar); + return; + } boost::apply_visitor( paramvalue_saver(ar), static_cast(*this) ); @@ -113,6 +118,10 @@ namespace alps { } std::ostream & operator<<(std::ostream & os, paramvalue const & arg) { + if (arg.source()) { + arg.source()->print(os); + return os; + } paramvalue_ostream visitor(os); boost::apply_visitor(visitor, arg); return os; diff --git a/src/alps/ngs/params.hpp b/src/alps/ngs/params.hpp index f43d47b05..f1f719fc6 100644 --- a/src/alps/ngs/params.hpp +++ b/src/alps/ngs/params.hpp @@ -32,6 +32,7 @@ #include #include #include +#include namespace alps { @@ -53,6 +54,7 @@ namespace alps { params(params const & arg) : keys(arg.keys) , values(arg.values) + , value_reader_(arg.value_reader_) {} params(hdf5::archive ar, std::string const & path = "/parameters"); @@ -84,6 +86,11 @@ namespace alps { void load(hdf5::archive &); + // A binding-owned decoder, preserved when parameters are copied + // into a native simulation. Native-only parameters need none. + typedef std::function value_reader; + void set_value_reader(value_reader reader) { value_reader_ = std::move(reader); } + #ifdef ALPS_HAVE_MPI void broadcast(boost::mpi::communicator const &, int = 0); #endif @@ -104,6 +111,7 @@ namespace alps { std::vector keys; std::map values; + value_reader value_reader_; }; ALPS_DECL std::ostream & operator<<(std::ostream & os, params const & arg); diff --git a/test/ngs/params/CMakeLists.txt b/test/ngs/params/CMakeLists.txt index 94387b6a2..2fcae5f36 100644 --- a/test/ngs/params/CMakeLists.txt +++ b/test/ngs/params/CMakeLists.txt @@ -22,9 +22,11 @@ include_directories(${PROJECT_BINARY_DIR}/src) include_directories(${PROJECT_SOURCE_DIR}/src) include_directories(${Boost_ROOT_DIR}) -FOREACH (name default ordering stream assign not_found) +FOREACH (name default ordering stream assign not_found external) add_executable(param_${name} ${name}.cpp) add_dependencies(param_${name} alps) target_link_libraries(param_${name} alps) add_alps_test(param_${name}) -ENDFOREACH(name) \ No newline at end of file +ENDFOREACH(name) + +target_link_libraries(param_external ${Boost_LIBRARIES}) diff --git a/test/ngs/params/external.cpp b/test/ngs/params/external.cpp new file mode 100644 index 000000000..8c5b5beb3 --- /dev/null +++ b/test/ngs/params/external.cpp @@ -0,0 +1,54 @@ +// Copyright (C) 2026 by the ALPS collaboration +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include + +void require(bool value) { + if (!value) throw std::runtime_error("external parameter contract failed"); +} + +struct source final : alps::detail::paramvalue_source { + std::vector values{1., 2.}; + alps::detail::paramvalue native_value() const override { return values; } + void save(alps::hdf5::archive & ar) const override { ar[""] << values; } + void print(std::ostream & out) const override { out << values.front(); } + void * object(char const *) const override { return nullptr; } +}; + +int main() { + auto value = std::make_shared(); + std::weak_ptr lifetime = value; + alps::params parameters; + parameters["vector"] = alps::detail::paramvalue(value); + alps::params copy(parameters); + value->values[0] = 9.; + require(copy["vector"].cast>()[0] == 9.); + require(parameters.find("vector")->cast>()[0] == 9.); + + // Boost serialization materializes native values instead of attempting + // to serialize a language runtime pointer or callback. + std::stringstream buffer; + { boost::archive::text_oarchive archive(buffer); archive << parameters; } + alps::params restored; + { boost::archive::text_iarchive archive(buffer); archive >> restored; } + require(restored["vector"].cast>() == value->values); + + parameters["vector"] = 3; + require(parameters["vector"].cast() == 3); + value.reset(); + require(!lifetime.expired()); + copy.erase("vector"); + require(lifetime.expired()); + + parameters["wide"] = std::string("9007199254740993"); + require(parameters["wide"].cast() == 9007199254740993LL); + try { + parameters["wide"].cast(); + throw std::runtime_error("overflowing conversion unexpectedly succeeded"); + } catch (std::out_of_range const &) {} +} diff --git a/test/pyalps/native_params/CMakeLists.txt b/test/pyalps/native_params/CMakeLists.txt new file mode 100644 index 000000000..5ab63b821 --- /dev/null +++ b/test/pyalps/native_params/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.22) +project(pyalps_parameter_contracts LANGUAGES CXX) +find_package(ALPS REQUIRED CONFIG) +find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) +execute_process(COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_VARIABLE _nanobind_dir OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) +list(PREPEND CMAKE_PREFIX_PATH "${_nanobind_dir}") +find_package(nanobind 2.10 CONFIG REQUIRED) +set(CMAKE_CXX_STANDARD 17) +nanobind_add_module(parameter_probe NB_STATIC probe.cpp) +target_include_directories(parameter_probe PRIVATE ${ALPS_INCLUDE_DIRS} ${ALPS_EXTRA_INCLUDE_DIRS}) +separate_arguments(_options NATIVE_COMMAND "${ALPS_CMAKE_CXX_FLAGS}") +target_compile_options(parameter_probe PRIVATE ${_options}) +target_compile_definitions(parameter_probe PRIVATE ${ALPS_EXTRA_DEFINITIONS}) +include("${ALPS_PYTHON_USE_FILE}") +alps_target_link_pyalps(parameter_probe PYTHON_EXECUTABLE "${Python_EXECUTABLE}") diff --git a/test/pyalps/native_params/check.py b/test/pyalps/native_params/check.py new file mode 100644 index 000000000..7104f5f9d --- /dev/null +++ b/test/pyalps/native_params/check.py @@ -0,0 +1,64 @@ +import copy +import gc +import tempfile +import weakref + +import numpy as np +import parameter_probe as native +from pyalps import hdf5, ngs + +empty = native.empty_vectors() +for key, kind in {"integer": "i", "real": "f", "complex": "c", "boolean": "b", "text": "U"}.items(): + assert empty[key].size == 0 and empty[key].dtype.kind == kind + +values = np.array([1.0, 2.0]) +parameters = ngs.params({"value": values}) +assert native.vector(parameters) == [1.0, 2.0] +values *= 3 +assert native.vector(parameters) == [3.0, 6.0] +parameters["value"][0] = 8 +assert native.threaded_vector(parameters) == [8.0, 6.0] +clone = native.clone(parameters) +parameters["value"][1] = 9 +assert native.vector(clone) == [8.0, 9.0] +native.replace(parameters) +assert native.vector(parameters) == [5.0, 6.0] +assert native.vector(clone) == [8.0, 9.0] +parameters["value"][0] = 10 +assert native.vector(parameters) == [10.0, 6.0] +parameters["value"] = [1.5, 2.5] +parameters["value"].append(3.5) +assert native.vector(parameters) == [1.5, 2.5, 3.5] +parameters["value"] = np.array([2 ** 40, 2 ** 40 + 1], dtype=np.int64) +assert native.vector(parameters) == [2 ** 40, 2 ** 40 + 1] +parameters["value"] = 2 ** 53 + 1 +assert native.wide_integer(parameters) == 2 ** 53 + 1 +try: + native.integer(parameters) +except (RuntimeError, TypeError, ValueError, IndexError, OverflowError): + pass +else: + raise AssertionError("out-of-range conversion to native int must fail") +parameters["value"] = np.array(2 ** 53 + 1, dtype=np.int64) +assert native.wide_integer(parameters) == 2 ** 53 + 1 + +with tempfile.TemporaryDirectory() as directory: + with hdf5.archive(directory + "/parameters.h5", "w") as archive: + archive["value"] = np.array([2.0, 4.0]) + archive["metadata"] = {"label": "test", "matrix": np.ones((2, 3))} + native.load(clone, archive) + assert native.threaded_vector(clone) == [2.0, 4.0] + assert clone["metadata"]["matrix"].shape == (2, 3) + clone["value"] *= 2 + assert native.vector(clone) == [4.0, 8.0] + +# Native destruction on a thread that started without the GIL must release +# the Python value safely, including when it owns the final reference. +value = np.ones(3) +reference = weakref.ref(value) +parameters = ngs.params({"value": value}) +del value +native.destroy_on_worker(parameters) +gc.collect() +assert reference() is None +print("native parameter contracts: ok") diff --git a/test/pyalps/native_params/probe.cpp b/test/pyalps/native_params/probe.cpp new file mode 100644 index 000000000..cdc19a7a5 --- /dev/null +++ b/test/pyalps/native_params/probe.cpp @@ -0,0 +1,44 @@ +// A separately compiled consumer catches ABI, cross-module and GIL errors. +#include +#include +#include +#include +#include +#include + +namespace nb = nanobind; +NB_MODULE(parameter_probe, module) { + nb::module_::import_("pyalps.ngs"); + module.def("vector", [](alps::params const & p) { return p["value"].cast>(); }); + module.def("integer", [](alps::params const & p) { return p["value"].cast(); }); + module.def("wide_integer", [](alps::params const & p) { return p["value"].cast(); }); + module.def("clone", [](alps::params const & p) { return alps::params(p); }); + module.def("replace", [](alps::params & p) { p["value"] = std::vector{5., 6.}; }); + module.def("empty_vectors", [] { + alps::params p; + p["integer"] = std::vector(); + p["real"] = std::vector(); + p["complex"] = std::vector>(); + p["boolean"] = std::vector(); + p["text"] = std::vector(); + return p; + }); + module.def("load", [](alps::params & p, alps::hdf5::archive & ar) { p.load(ar); }); + module.def("threaded_vector", [](alps::params const & p) { + std::vector values; + std::exception_ptr error; + std::thread worker([&] { + try { values = p["value"].cast>(); } + catch (...) { error = std::current_exception(); } + }); + worker.join(); + if (error) std::rethrow_exception(error); + return values; + }, nb::call_guard()); + module.def("destroy_on_worker", [](alps::params & p) { + auto copy = std::make_unique(p); + p.erase("value"); + std::thread worker([copy = std::move(copy)]() mutable { copy.reset(); }); + worker.join(); + }, nb::call_guard()); +} diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 45e63456f..836b3b1ea 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -629,6 +629,27 @@ def test_current_python_numpy_and_scipy_compatibility(monkeypatch): assert isinstance(steady["value"], (bool, np.bool_)) +@pytest.mark.skipif(os.environ.get("PYALPS_TEST_DOWNSTREAM_EXPORT") != "1", + reason="compiled consumer enabled once per platform in CI") +def test_native_parameter_contracts(tmp_path): + repository = Path(__file__).resolve().parents[2] + source = repository / "test" / "pyalps" / "native_params" + build = tmp_path / "native-params" + subprocess.run([ + "cmake", "-S", str(source), "-B", str(build), + "-DALPS_DIR=" + str(repository / "_build/wheel-deps/install/share/alps"), + "-DPython_EXECUTABLE=" + sys.executable, + ], check=True) + subprocess.run(["cmake", "--build", str(build), "--parallel", "2"], check=True) + completed = subprocess.run( + [sys.executable, "-X", "faulthandler", str(source / "check.py")], + env={**os.environ, "PYTHONPATH": str(build), "MallocScribble": "1"}, + capture_output=True, text=True, timeout=60, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert "native parameter contracts: ok" in completed.stdout + + def test_params_mapping_equality_and_value_ladder(): from pyalps import ngs @@ -644,110 +665,27 @@ def test_params_mapping_equality_and_value_ladder(): except TypeError: pass + # Python values must retain their user-visible behavior. Native C++ + # conversion is tested by the downstream parameter probe, not by forcing + # Python lookups to adopt the restricted native variant's types. + values = [ + None, 2 ** 40, [2 ** 53 + 1], [True, False], [True, 1], + np.bool_(True), np.int64(8), np.float32(1.25), np.longdouble("1.125"), + np.complex64(1 + 2j), np.clongdouble(3 + 4j), np.bytes_(b"native"), + [np.int64(1), np.int64(2)], np.array([1, 2], dtype=np.int64), + np.ma.array([1, 2], mask=False), np.array([1.5, 2.5], dtype=np.float32), + np.array([1 + 2j, 3 + 4j], dtype=np.complex64), + np.array([1 + 2j, 3 + 4j], dtype=np.clongdouble), + [np.complex64(5 + 6j), np.clongdouble(7 + 8j)], + np.array(["a", "b"]), np.array([b"a", b"b"], dtype="S1"), + np.array(7, dtype=np.int64), np.array([], dtype=np.bool_), + np.ones((2, 2)), [np.int64(2 ** 40)], [1, 2, 3], [1.5, 2.5], + ["a", "b"], [1, 2.5], 1 + 2j, {"nested": 1}, object(), (1, 2), + ] p = ngs.params({}) - # None is rejected with a message that says so - try: - p["x"] = None - raise AssertionError("None must be rejected") - except TypeError as error: - assert "None" in str(error) - # oversized integers raise instead of truncating silently — - # inside lists too, where the double-widening fallback would - # otherwise corrupt values beyond 2**53 - try: - p["n"] = 2 ** 40 - raise AssertionError("2**40 must be rejected") - except TypeError as error: - assert "32-bit" in str(error) - try: - p["nl"] = [2 ** 53 + 1] - raise AssertionError("[2**53+1] must be rejected") - except TypeError as error: - assert "32-bit" in str(error) - # Homogeneous bool sequences have a native C++ representation and - # round-trip without falling back to stored Python objects. - p["flags"] = [True, False] - assert p["flags"] == [True, False] - p["npflags"] = np.array([True, False], dtype=np.bool_) - assert p["npflags"] == [True, False] - try: - p["mixedflags"] = [True, 1] - raise AssertionError("mixed bool/numeric sequences must be rejected") - except TypeError as error: - assert "cannot be mixed" in str(error) - # numpy integer scalars are accepted like numpy floats are — - # as scalars and inside lists, with the same 32-bit range policy - p["npint"] = np.int64(8) - assert p["npint"] == 8 and type(p["npint"]) is int - p["npbool"] = np.bool_(True) - assert p["npbool"] is True - p["npfloat32"] = np.float32(1.25) - assert p["npfloat32"] == 1.25 - p["nplongdouble"] = np.longdouble("1.125") - assert p["nplongdouble"] == 1.125 - p["npcomplex64"] = np.complex64(1 + 2j) - assert p["npcomplex64"] == 1 + 2j - p["npclongdouble"] = np.clongdouble(3 + 4j) - assert p["npclongdouble"] == 3 + 4j - p["npbytes"] = np.bytes_(b"native") - assert p["npbytes"] == "native" - p["npints"] = [np.int64(1), np.int64(2)] - assert p["npints"] == [1, 2] - assert all(type(v) is int for v in p["npints"]) - p["nparray"] = np.array([1, 2], dtype=np.int64) - assert p["nparray"] == [1, 2] - p["npsubclass"] = np.ma.array([1, 2], mask=False) - assert p["npsubclass"] == [1, 2] - p["npfloats"] = np.array([1.5, 2.5], dtype=np.float32) - assert p["npfloats"] == [1.5, 2.5] - p["npcomplex"] = np.array([1 + 2j, 3 + 4j], dtype=np.complex64) - assert p["npcomplex"] == [1 + 2j, 3 + 4j] - p["npextendedcomplex"] = np.array( - [1 + 2j, 3 + 4j], dtype=np.clongdouble - ) - assert p["npextendedcomplex"] == [1 + 2j, 3 + 4j] - p["npcomplexlist"] = [np.complex64(5 + 6j), np.clongdouble(7 + 8j)] - assert p["npcomplexlist"] == [5 + 6j, 7 + 8j] - p["npstrings"] = np.array(["a", "b"]) - assert p["npstrings"] == ["a", "b"] - p["npbytestrings"] = np.array([b"a", b"b"], dtype="S1") - assert p["npbytestrings"] == ["a", "b"] - p["np0d"] = np.array(7, dtype=np.int64) - assert p["np0d"] == 7 - p["emptyflags"] = np.array([], dtype=np.bool_) - assert p["emptyflags"] == [] - try: - p["matrix"] = np.ones((2, 2)) - raise AssertionError("multidimensional parameter arrays must be rejected") - except TypeError as error: - assert "multidimensional" in str(error) - try: - p["npbig"] = [np.int64(2 ** 40)] - raise AssertionError("[np.int64(2**40)] must be rejected") - except TypeError as error: - assert "32-bit" in str(error) - # exact-type lists round-trip with their element type - p["ilist"] = [1, 2, 3] - assert p["ilist"] == [1, 2, 3] - assert all(type(v) is int for v in p["ilist"]) - p["flist"] = [1.5, 2.5] - assert p["flist"] == [1.5, 2.5] - p["slist"] = ["a", "b"] - assert p["slist"] == ["a", "b"] - # mixed numeric lists widen to double; complex scalars are stored - p["mixed"] = [1, 2.5] - assert p["mixed"] == [1.0, 2.5] - p["cplx"] = 1 + 2j - assert p["cplx"] == 1 + 2j - - # Unsupported object graphs stay unsupported: params owns only native - # C++ values and must never keep arbitrary Python objects alive. - for unsupported in ({"nested": 1}, object()): - try: - p["object"] = unsupported - raise AssertionError("arbitrary Python objects must be rejected") - except TypeError: - pass + for index, value in enumerate(values): + p[str(index)] = value + assert p[str(index)] is value def test_params_mapping_mixins_handle_none_getitem(): diff --git a/tutorials/ngs/5_export_python/smoke_test.py b/tutorials/ngs/5_export_python/smoke_test.py index 20da2a40d..da9f15637 100644 --- a/tutorials/ngs/5_export_python/smoke_test.py +++ b/tutorials/ngs/5_export_python/smoke_test.py @@ -3,6 +3,7 @@ import os import tempfile +import numpy as np # Importing the consumer first verifies its wheel-runtime rpath. Its module # initializer loads the owning pyalps bindings before registering C++ types. @@ -11,7 +12,9 @@ import pyalps.ngs as ngs -parameters = ngs.params({"SEED": 7, "SWEEPS": 10}) +parameters = ngs.params({"SEED": 7, "SWEEPS": 10, + "couplings": np.array([1., 2.]), + "metadata": {"label": "checkpoint", "matrix": np.ones((2, 3))}}) simulation = ising_c.sim(parameters) assert issubclass(ising_c.sim, ngs.mcbase) @@ -29,6 +32,7 @@ assert simulation.resultNames() == ["Magnetization"] before = simulation.collectResults() assert before["Magnetization"].count == 10 +simulation.parameters["couplings"] *= 3 with tempfile.TemporaryDirectory() as directory: checkpoint = os.path.join(directory, "ising.h5") @@ -40,6 +44,10 @@ restored.load(archive) after = restored.collectResults() + np.testing.assert_array_equal(restored.parameters["couplings"], [3., 6.]) + assert restored.parameters["metadata"]["matrix"].shape == (2, 3) + restored.parameters["couplings"][0] = 4 + assert restored.parameters["couplings"][0] == 4 assert restored.resultNames() == simulation.resultNames() assert after["Magnetization"].count == before["Magnetization"].count assert after["Magnetization"].mean == before["Magnetization"].mean From 3e6df562e66929e3da2c2821ad5b19993be0cb8b Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 22:44:23 -0400 Subject: [PATCH 45/52] Restore safe result checkpoint loading and value operations --- .../python/pyalps/cpp/ngs/observables.cpp | 7 +- bindings/python/pyalps/cpp/ngs/result.cpp | 12 +- bindings/python/pyalps/cpp/ngs/results.cpp | 12 +- src/alps/ngs/lib/mcresult.cpp | 106 ++++++----- src/alps/ngs/lib/mcresult_impl_derived.ipp | 20 +- src/alps/ngs/lib/mcresults.cpp | 12 +- src/alps/ngs/mcresult.hpp | 1 + src/alps/numeric/functional.hpp | 4 + test/pyalps/test_checkpoint_contracts.py | 172 ++++++++++++++++++ 9 files changed, 279 insertions(+), 67 deletions(-) create mode 100644 test/pyalps/test_checkpoint_contracts.py diff --git a/bindings/python/pyalps/cpp/ngs/observables.cpp b/bindings/python/pyalps/cpp/ngs/observables.cpp index 40db03d96..bce51a3e8 100644 --- a/bindings/python/pyalps/cpp/ngs/observables.cpp +++ b/bindings/python/pyalps/cpp/ngs/observables.cpp @@ -49,10 +49,9 @@ namespace nb = nanobind; #include namespace { void mcobservables_load(alps::mcobservables & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); + alps::hdf5::archive reader(ar); + reader.set_context(ar.complete_path(path)); + self.load(reader); } void createRealObservable(alps::mcobservables & self, std::string const & name, std::uint32_t binnum) { self << alps::ngs::RealObservable(name, binnum); diff --git a/bindings/python/pyalps/cpp/ngs/result.cpp b/bindings/python/pyalps/cpp/ngs/result.cpp index 11ad2ab9f..6c4fd7405 100644 --- a/bindings/python/pyalps/cpp/ngs/result.cpp +++ b/bindings/python/pyalps/cpp/ngs/result.cpp @@ -120,7 +120,7 @@ NB_MODULE(pyngsresult_c, m) { .def("__repr__", &alps::detail::mcresult_print) .def("__deepcopy__", [](R const & self, nb::handle /*memo*/) { - return R(self); + return self.count() ? self + 0.0 : R(); }) .def("__abs__", static_cast(&abs)) .def("__pow__", static_cast(&pow)) @@ -133,8 +133,14 @@ NB_MODULE(pyngsresult_c, m) { // mcresult's unary +/- operate on non-const self and return a // reference (not a new value). Wrap them in lambdas that // return a fresh copy, which is what Python's +obj/-obj expect. - .def("__pos__", [](R self) { return +self; }) - .def("__neg__", [](R self) { return -self; }) + .def("__pos__", [](R const & self) { + if (!self.count()) throw nb::value_error("result has no measurements"); + return self + 0.0; + }) + .def("__neg__", [](R const & self) { + if (!self.count()) throw nb::value_error("result has no measurements"); + return self * -1.0; + }) // In-place operators — return self by reference so the original // object is modified in place (Python's __i*__ semantics). .def("__iadd__", [](R & self, R const & o) -> R & { return self += o; }, nb::is_operator()) diff --git a/bindings/python/pyalps/cpp/ngs/results.cpp b/bindings/python/pyalps/cpp/ngs/results.cpp index 1663aeaed..5a802f93b 100644 --- a/bindings/python/pyalps/cpp/ngs/results.cpp +++ b/bindings/python/pyalps/cpp/ngs/results.cpp @@ -26,15 +26,19 @@ namespace alps { return sstr.str(); } void mcresults_load(alps::mcresults & self, alps::hdf5::archive & ar, std::string const & path) { - std::string current = ar.get_context(); - ar.set_context(path); - self.load(ar); - ar.set_context(current); + alps::hdf5::archive reader(ar); + reader.set_context(ar.complete_path(path)); + alps::mcresults loaded; + loaded.load(reader); + while (!self.empty()) + pyalps::erase_map_item(self, self.begin()->first); + self.swap(loaded); } } } NB_MODULE(pyngsresults_c, m) { nb::class_(m, "results") + .def(nb::init<>()) .def("__len__", [](alps::mcresults const & self) { return self.size(); }) .def("__contains__", [](alps::mcresults const & self, std::string const & k) { return self.has(k); diff --git a/src/alps/ngs/lib/mcresult.cpp b/src/alps/ngs/lib/mcresult.cpp index bd3f88866..b338c192f 100644 --- a/src/alps/ngs/lib/mcresult.cpp +++ b/src/alps/ngs/lib/mcresult.cpp @@ -27,9 +27,17 @@ #include #include +#include +#include namespace alps { + detail::mcresult_impl_base * mcresult::implementation() const { + if (!impl_) + throw std::runtime_error("Result has no measurements" + ALPS_STACKTRACE); + return impl_; + } + mcresult::mcresult() : impl_(NULL) {} @@ -65,7 +73,8 @@ namespace alps { // #endif mcresult::mcresult(mcresult const & rhs) { - ++ref_cnt_[impl_ = rhs.impl_]; + impl_ = rhs.impl_; + if (impl_) ++ref_cnt_[impl_]; } mcresult::mcresult(mcobservable const & obs) { @@ -73,41 +82,41 @@ namespace alps { } mcresult::~mcresult() { - if (impl_ && !--ref_cnt_[impl_]) + if (impl_ && !--ref_cnt_[impl_]) { + ref_cnt_.erase(impl_); delete impl_; + } } mcresult & mcresult::operator=(mcresult rhs) { - if (impl_ && !--ref_cnt_[impl_]) - delete impl_; - ++ref_cnt_[impl_ = rhs.impl_]; + std::swap(impl_, rhs.impl_); return *this; } #define ALPS_MCRESULT_TPL_IMPL(T) \ - template<> ALPS_DECL bool mcresult::is_type< T >() const { return impl_->is_type< T >(); } \ - template<> ALPS_DECL std::vector< T > const & mcresult::bins< T >() const { return impl_->bins< T >(); } \ - template<> ALPS_DECL T const & mcresult::mean< T >() const { return impl_->mean< T >(); } \ - template<> ALPS_DECL T const & mcresult::error< T >() const { return impl_->error< T >(); } \ - template<> ALPS_DECL T const & mcresult::variance< T >() const { return impl_->variance< T >(); } \ - template<> ALPS_DECL T const & mcresult::tau< T >() const { return impl_->tau< T >(); } \ + template<> ALPS_DECL bool mcresult::is_type< T >() const { return impl_ && implementation()->is_type< T >(); } \ + template<> ALPS_DECL std::vector< T > const & mcresult::bins< T >() const { return implementation()->bins< T >(); } \ + template<> ALPS_DECL T const & mcresult::mean< T >() const { return implementation()->mean< T >(); } \ + template<> ALPS_DECL T const & mcresult::error< T >() const { return implementation()->error< T >(); } \ + template<> ALPS_DECL T const & mcresult::variance< T >() const { return implementation()->variance< T >(); } \ + template<> ALPS_DECL T const & mcresult::tau< T >() const { return implementation()->tau< T >(); } \ template<> ALPS_DECL covariance_type::type mcresult::covariance< T >(mcresult const & arg) const { \ - return impl_->covariance< T >(*arg.impl_); \ + return implementation()->covariance< T >(*arg.implementation()); \ } \ template<> ALPS_DECL covariance_type::type mcresult::accurate_covariance< T >(mcresult const & arg) const { \ - return impl_->accurate_covariance< T >(*arg.impl_); \ + return implementation()->accurate_covariance< T >(*arg.implementation()); \ } \ - template<> ALPS_DECL mcresult & mcresult::operator+=< T >( T const & rhs) { impl_->add_assign(rhs); return *this; } \ - template<> ALPS_DECL mcresult & mcresult::operator-=< T >( T const & rhs) { impl_->sub_assign(rhs); return *this; } \ - template<> ALPS_DECL mcresult & mcresult::operator*=< T >( T const & rhs) { impl_->mul_assign(rhs); return *this; } \ - template<> ALPS_DECL mcresult & mcresult::operator/=< T >( T const & rhs) { impl_->div_assign(rhs); return *this; } + template<> ALPS_DECL mcresult & mcresult::operator+=< T >( T const & rhs) { implementation()->add_assign(rhs); return *this; } \ + template<> ALPS_DECL mcresult & mcresult::operator-=< T >( T const & rhs) { implementation()->sub_assign(rhs); return *this; } \ + template<> ALPS_DECL mcresult & mcresult::operator*=< T >( T const & rhs) { implementation()->mul_assign(rhs); return *this; } \ + template<> ALPS_DECL mcresult & mcresult::operator/=< T >( T const & rhs) { implementation()->div_assign(rhs); return *this; } ALPS_MCRESULT_TPL_IMPL(double) ALPS_MCRESULT_TPL_IMPL(std::vector) #undef ALPS_MCRESULT_TPL_IMPL #define ALPS_NGS_MCRESULT_OPERATOR_IMPL(OP, NAME) \ mcresult & mcresult:: OP (mcresult const & rhs) { \ - impl_-> NAME ## _assign (rhs.impl_); \ + implementation()-> NAME ## _assign (rhs.implementation()); \ return *this; \ } ALPS_NGS_MCRESULT_OPERATOR_IMPL(operator+=, add) @@ -117,61 +126,77 @@ namespace alps { #undef ALPS_NGS_MCRESULT_OPERATOR_IMPL bool mcresult::can_rebin() const { - return impl_->can_rebin(); + return implementation()->can_rebin(); } bool mcresult::jackknife_valid() const { - return impl_->jackknife_valid(); + return implementation()->jackknife_valid(); } uint64_t mcresult::count() const { - return impl_->count(); + return impl_ ? implementation()->count() : 0; } uint64_t mcresult::bin_size() const { - return impl_->bin_size(); + return implementation()->bin_size(); } uint64_t mcresult::max_bin_number() const { - return impl_->max_bin_number(); + return implementation()->max_bin_number(); } std::size_t mcresult::bin_number() const { - return impl_->bin_number(); + return implementation()->bin_number(); } bool mcresult::has_variance() const { - return impl_->has_variance(); + return implementation()->has_variance(); } bool mcresult::has_tau() const { - return impl_->has_tau(); + return implementation()->has_tau(); } void mcresult::set_bin_size(uint64_t binsize) { - impl_->set_bin_size(binsize); + implementation()->set_bin_size(binsize); } void mcresult::set_bin_number(uint64_t bin_number) { - impl_->set_bin_number(bin_number); + implementation()->set_bin_number(bin_number); } void mcresult::save(hdf5::archive & ar) const { - impl_->save(ar); + if (!impl_) + throw std::runtime_error("Cannot save an uninitialized result" + ALPS_STACKTRACE); + implementation()->save(ar); } void mcresult::load(hdf5::archive & ar) { - impl_->save(ar); + // Read into an independent payload before touching this result. This + // also discovers the type of a default-constructed result and keeps + // aliases and the old value intact if any archive read fails. + std::unique_ptr payload; + if (ar.is_scalar("mean/value")) + payload.reset(new detail::mcresult_impl_derived(alea::mcdata())); + else if (ar.dimensions("mean/value") == 1) + payload.reset(new detail::mcresult_impl_derived >(alea::mcdata >())); + else + throw std::runtime_error("Unsupported result shape" + ALPS_STACKTRACE); + payload->load(ar); + mcresult replacement; + ref_cnt_[payload.get()] = 1; + replacement.impl_ = payload.release(); + std::swap(impl_, replacement.impl_); } void mcresult::output(std::ostream & os) const { - impl_->output(os); + implementation()->output(os); } #ifdef ALPS_HAVE_MPI mcresult mcresult::reduce(boost::mpi::communicator const & communicator, std::size_t binnumber) { mcresult lhs; - detail::mcresult_impl_base * impl = impl_->reduce(communicator, binnumber); + detail::mcresult_impl_base * impl = implementation()->reduce(communicator, binnumber); if (communicator.rank() == 0) ref_cnt_[lhs.impl_ = impl] = 1; return lhs; @@ -179,18 +204,17 @@ namespace alps { #endif bool mcresult::operator== (mcresult const & rhs) const { - return impl_->operator== (rhs.impl_); + return implementation()->operator== (rhs.implementation()); } bool mcresult::operator!= (mcresult const & rhs) const { - return impl_->operator!= (rhs.impl_); + return implementation()->operator!= (rhs.implementation()); } mcresult & mcresult::operator+() { - impl_->operator-(); return *this; } mcresult & mcresult::operator-() { - impl_->operator-(); + implementation()->operator-(); return *this; } @@ -219,7 +243,7 @@ namespace alps { #define ALPS_NGS_MCRESULT_FREE_UNITARY_FUN(FUN_NAME) \ mcresult FUN_NAME (mcresult rhs) { \ mcresult lhs; \ - lhs.ref_cnt_[lhs.impl_ = rhs.impl_-> FUN_NAME ()] = 1; \ + lhs.ref_cnt_[lhs.impl_ = rhs.implementation()-> FUN_NAME ()] = 1; \ return lhs; \ } ALPS_NGS_MCRESULT_FREE_UNITARY_FUN(sin) @@ -246,19 +270,19 @@ namespace alps { mcresult pow(mcresult rhs, double exponent) { mcresult lhs; - lhs.ref_cnt_[lhs.impl_ = rhs.impl_->pow(exponent)] = 1; + lhs.ref_cnt_[lhs.impl_ = rhs.implementation()->pow(exponent)] = 1; return lhs; } #define ALPS_NGS_MCRESULT_FREE_OPERATOR_TPL_IMPL(T, OP, NAME) \ mcresult OP(mcresult const & lhs, T const & rhs) { \ mcresult res; \ - res.ref_cnt_[res.impl_ = lhs.impl_-> NAME (rhs)] = 1; \ + res.ref_cnt_[res.impl_ = lhs.implementation()-> NAME (rhs)] = 1; \ return res; \ } \ mcresult OP(T const & lhs, mcresult const & rhs) { \ mcresult res; \ - res.ref_cnt_[res.impl_ = rhs.impl_-> NAME ## _inverse (lhs)] = 1; \ + res.ref_cnt_[res.impl_ = rhs.implementation()-> NAME ## _inverse (lhs)] = 1; \ return res; \ } #define ALPS_NGS_MCRESULT_FREE_OPERATOR_IMPL(OP, NAME) \ @@ -266,7 +290,7 @@ namespace alps { ALPS_NGS_MCRESULT_FREE_OPERATOR_TPL_IMPL(std::vector, OP, NAME) \ mcresult OP (mcresult const & lhs, mcresult const & rhs) { \ mcresult res; \ - res.ref_cnt_[res.impl_ = lhs.impl_-> NAME (rhs.impl_)] = 1; \ + res.ref_cnt_[res.impl_ = lhs.implementation()-> NAME (rhs.implementation())] = 1; \ return res; \ } ALPS_NGS_MCRESULT_FREE_OPERATOR_IMPL(operator+, add) diff --git a/src/alps/ngs/lib/mcresult_impl_derived.ipp b/src/alps/ngs/lib/mcresult_impl_derived.ipp index b88d2e9d0..09d2e477f 100644 --- a/src/alps/ngs/lib/mcresult_impl_derived.ipp +++ b/src/alps/ngs/lib/mcresult_impl_derived.ipp @@ -133,29 +133,29 @@ namespace alps { return alea::mcdata::accurate_covariance(static_cast const &>(arg)); } #define ALPS_NGS_MCRESULT_IMPL_DERIVED_OPERATOR(NAME, OP, OP_ASSIGN) \ - template typename boost::enable_if< \ + template typename boost::enable_if::type \ - /*, typename boost::is_same::element_type, U>::type*/ \ - >::type NAME ## _assign (U const & rhs) { \ + , typename boost::is_same::element_type, U>::type \ + >::type>::type NAME ## _assign (U const & rhs) { \ static_cast &>(*this) OP_ASSIGN rhs; \ } \ \ - template typename boost::disable_if< \ + template typename boost::disable_if::type \ - /*, typename boost::is_same::element_type, U>::type*/ \ - >::type NAME ## _assign (U const & rhs) { \ + , typename boost::is_same::element_type, U>::type \ + >::type>::type NAME ## _assign (U const & rhs) { \ throw std::runtime_error("Invalid cast" + ALPS_STACKTRACE); \ } \ \ void NAME ## _assign_virtual (B const * rhs) { \ static_cast &>(*this) \ - OP_ASSIGN static_cast const &>(*dynamic_cast const *>(rhs)); \ + OP_ASSIGN static_cast const &>(dynamic_cast const &>(*rhs)); \ } \ \ \ template typename boost::enable_if::type \ - /*, typename boost::is_same::element_type, U>::type*/ \ + , typename boost::is_same::element_type, U>::type \ >::type, B *>::type NAME (U const & rhs) const { \ return new mcresult_impl_derived( \ static_cast const &>(*this) OP rhs \ @@ -164,7 +164,7 @@ namespace alps { \ template typename boost::disable_if::type \ - /* , typename boost::is_same::element_type, U>::type*/ \ + , typename boost::is_same::element_type, U>::type \ >::type, B *>::type NAME (U const & rhs) const { \ throw std::runtime_error("Invalid cast" + ALPS_STACKTRACE); \ return NULL; \ @@ -262,7 +262,7 @@ namespace alps { } void load(hdf5::archive & ar) { - alea::mcdata::save(ar); + alea::mcdata::load(ar); } void output(std::ostream & os) const { diff --git a/src/alps/ngs/lib/mcresults.cpp b/src/alps/ngs/lib/mcresults.cpp index ae46debf6..6e265b763 100644 --- a/src/alps/ngs/lib/mcresults.cpp +++ b/src/alps/ngs/lib/mcresults.cpp @@ -58,11 +58,13 @@ namespace alps { } void mcresults::load(hdf5::archive & ar) { - ObservableSet set; - // TODO: do not use hard coded path! - ar >> make_pvp("/simulation/realizations/0/clones/0/results", set); - for(ObservableSet::const_iterator it = set.begin(); it != set.end(); ++it) - insert(it->first, mcresult(it->second)); + mcresults loaded; + for (auto const & child : ar.list_children(ar.get_context())) { + mcresult result; + ar[child] >> result; + loaded.insert(ar.decode_segment(child), result); + } + swap(loaded); } void mcresults::output(std::ostream & os) const { diff --git a/src/alps/ngs/mcresult.hpp b/src/alps/ngs/mcresult.hpp index 2cdd18ff0..9fd5573ae 100644 --- a/src/alps/ngs/mcresult.hpp +++ b/src/alps/ngs/mcresult.hpp @@ -186,6 +186,7 @@ namespace alps { private: void construct(Observable const * obs); + detail::mcresult_impl_base * implementation() const; detail::mcresult_impl_base * impl_; static std::map ref_cnt_; diff --git a/src/alps/numeric/functional.hpp b/src/alps/numeric/functional.hpp index 9c030f8ad..3ac1b3be8 100644 --- a/src/alps/numeric/functional.hpp +++ b/src/alps/numeric/functional.hpp @@ -33,12 +33,14 @@ namespace alps { template struct plus { R operator()(T const & x, U const & y) const { using boost::numeric::operators::operator+; + using alps::numeric::operator+; return x + y; } }; template struct plus { T operator()(T const & x, T const & y) const { using boost::numeric::operators::operator+; + using alps::numeric::operator+; return x + y; } }; @@ -46,12 +48,14 @@ namespace alps { template struct minus { R operator()(T const & x, U const & y) const { using boost::numeric::operators::operator-; + using alps::numeric::operator-; return x - y; } }; template struct minus { T operator()(T const & x, T const & y) const { using boost::numeric::operators::operator-; + using alps::numeric::operator-; return x - y; } }; diff --git a/test/pyalps/test_checkpoint_contracts.py b/test/pyalps/test_checkpoint_contracts.py new file mode 100644 index 000000000..347735e95 --- /dev/null +++ b/test/pyalps/test_checkpoint_contracts.py @@ -0,0 +1,172 @@ +"""User workflows discovered by auditing the migration beyond API presence.""" + +import copy +import gc +import os +import subprocess +import sys +import textwrap +import weakref + +import numpy as np +import pytest + +from pyalps import hdf5, ngs + + +def result(vector=False, offset=0): + observable = (ngs.createRealVectorObservable if vector else ngs.createRealObservable)("samples") + for i in range(64): + observable << (np.array([i + offset, 2.0 * i]) if vector else float(i + offset)) + return ngs.observable2result(observable) + + +def test_parameters_preserve_arithmetic_mutation_and_ownership(tmp_path): + array = np.array([1.0, 2.0]) + reference = weakref.ref(array) + parameters = ngs.params({"array": array, "list": [1, 2], "tuple": (3, 4)}) + np.testing.assert_array_equal(parameters["array"] * 2, [2, 4]) + array[0] = 9 + del array + gc.collect() + assert reference() is not None + parameters["array"] += 1 + parameters["list"].append(3) + parameters["list"][0] = 8 + assert parameters["tuple"] * 2 == (3, 4, 3, 4) + duplicate = copy.deepcopy(parameters) + duplicate["array"][0] = -1 + duplicate["list"].clear() + np.testing.assert_array_equal(parameters["array"], [10, 3]) + assert parameters["list"] == [8, 2, 3] + with hdf5.archive(str(tmp_path / "parameters.h5"), "w") as archive: + archive["parameters"] = parameters + np.testing.assert_array_equal(archive["parameters/array"], [10, 3]) + np.testing.assert_array_equal(archive["parameters/list"], [8, 2, 3]) + del parameters["array"] + gc.collect() + assert reference() is None + + +def test_parameter_checkpoint_retains_arrays_metadata_and_large_integers(tmp_path): + values = {"array": np.arange(6.0).reshape(2, 3), "large": 2 ** 53 + 1, + "metadata": {"label": "run", "ids": [1, 2]}, "flags": [True, False]} + filename = str(tmp_path / "parameters.h5") + with hdf5.archive(filename, "w") as archive: + archive["custom"] = ngs.params(values) + with hdf5.archive(filename, "r") as archive: + parameters = ngs.params(archive, "/custom") + assert archive.context == "/" + np.testing.assert_array_equal(parameters["array"] * 2, values["array"] * 2) + assert parameters["large"] == 2 ** 53 + 1 + assert parameters["metadata"]["label"] == "run" + assert parameters["flags"] == [True, False] + parameters["array"][0, 0] = 99 + with hdf5.archive(filename, "a") as archive: + archive["again"] = parameters + assert archive["again/array"][0, 0] == 99 + + +@pytest.mark.parametrize("vector", [False, True]) +def test_result_loads_default_and_existing_objects_without_writing(tmp_path, vector): + original = result(vector) + filename = str(tmp_path / "result.h5") + with hdf5.archive(filename, "w") as archive: + archive["result"] = original + restored = ngs.result() + assert restored.count == 0 and repr(restored) == "No Measurements" + with hdf5.archive(filename, "r") as archive: + archive.set_context("/result") + restored.load(archive) + assert archive.context == "/result" + np.testing.assert_array_equal(restored.mean, original.mean) + np.testing.assert_array_equal(restored.error, original.error) + assert restored.count == original.count + alias = ngs.result(restored) + replacement = result(not vector, offset=100) + with hdf5.archive(filename, "w") as archive: + archive["result"] = replacement + with hdf5.archive(filename, "r") as archive: + archive.set_context("/result") + restored.load(archive) + np.testing.assert_array_equal(alias.mean, original.mean) + np.testing.assert_array_equal(restored.mean, replacement.mean) + + +def test_result_collection_load_uses_context_and_keeps_old_references(tmp_path): + first = ngs.results() + first["energy/with&name"] = result() + first["vector"] = result(True) + filename = str(tmp_path / "results.h5") + with hdf5.archive(filename, "w") as archive: + archive["custom/results"] = first + restored = ngs.results() + restored["old"] = result(offset=10) + old_reference = restored["old"] + with hdf5.archive(filename, "r") as archive: + restored.load(archive, "/custom/results") + assert archive.context == "/" + assert set(restored) == set(first) + energy_reference = restored["energy/with&name"] + restored.load(archive, "/custom/results") + del restored + gc.collect() + assert old_reference.mean == 41.5 + assert energy_reference.mean == 31.5 + + +def test_failed_loads_preserve_values_and_context(tmp_path): + filename = str(tmp_path / "broken.h5") + with hdf5.archive(filename, "w") as archive: + archive["bad/mean/value"] = 100.0 # no mandatory count dataset + value = result() + with hdf5.archive(filename, "r") as archive: + archive.set_context("/bad") + with pytest.raises(Exception): + value.load(archive) + assert archive.context == "/bad" + assert value.mean == 31.5 + archive.set_context("/") + parameters = ngs.params({"kept": [1, 2]}) + with pytest.raises(Exception): + parameters.load(archive, "/missing") + assert archive.context == "/" and parameters["kept"] == [1, 2] + results = ngs.results() + results["kept"] = value + with pytest.raises(Exception): + results.load(archive, "/bad") + assert archive.context == "/" and results["kept"].mean == 31.5 + + +@pytest.mark.parametrize("vector", [False, True]) +def test_result_unary_operations_and_deepcopy_do_not_mutate_original(vector): + original = result(vector) + mean = np.array(original.mean) + positive, negative, duplicate = +original, -original, copy.deepcopy(original) + duplicate += 10 + np.testing.assert_array_equal(original.mean, mean) + np.testing.assert_array_equal(positive.mean, mean) + np.testing.assert_array_equal(negative.mean, -mean) + np.testing.assert_array_equal(duplicate.mean, mean + 10) + + +def test_empty_result_operations_raise_instead_of_crashing(): + code = ''' + from pyalps import ngs + result = ngs.result() + for operation in (lambda: result + 1, lambda: abs(result), + lambda: result ** 2, lambda: result.sin(), + lambda: result.mean, lambda: result.error): + try: + operation() + except (RuntimeError, ValueError): + pass + else: + raise AssertionError("empty result operation should fail") + ''' + completed = subprocess.run( + [sys.executable, "-X", "faulthandler", "-c", textwrap.dedent(code)], + capture_output=True, text=True, timeout=30, + env={**os.environ, "MallocScribble": "1"}, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr From 3b451b2b9b054faf01da58f2a4facb8d658bbc36 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 23:00:20 -0400 Subject: [PATCH 46/52] Preserve heterogeneous parameter conversion for native consumers --- bindings/python/pyalps/cpp/dict_to_params.hpp | 15 ++++++++++++++ src/alps/ngs/detail/paramvalue.hpp | 20 ++++++++++++++++++- test/pyalps/native_params/check.py | 15 ++++++++++++++ test/pyalps/native_params/probe.cpp | 3 +++ test/pyalps/test_binding_surface.py | 2 ++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index 6f6688116..310960c56 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -302,6 +302,21 @@ class python_paramvalue_source final : public alps::detail::paramvalue_source { Py_DECREF(value_); } } + bool native_elements(std::vector & elements) const override { + nb::gil_scoped_acquire gil; + nb::object items = nb::borrow(value_); + if (detail::is_numpy_array(items)) { + if (nb::cast(items.attr("ndim")) != 1) + return false; + items = items.attr("tolist")(); + } + if (!nb::isinstance(items) && !nb::isinstance(items)) + return false; + elements.reserve(nb::len(items)); + for (std::size_t i = 0; i < nb::len(items); ++i) + elements.push_back(python_paramvalue_source(items[i], key_).native_value()); + return true; + } alps::detail::paramvalue native_value() const override { nb::gil_scoped_acquire gil; // Defer large integers to ALPS' checked text-to-target conversion. diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index 4ca192118..8e8d19226 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -98,6 +98,10 @@ namespace alps { struct ALPS_DECL paramvalue_source { virtual ~paramvalue_source(); virtual paramvalue native_value() const = 0; + // A heterogeneous sequence must be converted element by element + // to the type requested by the consumer, without first forcing + // every element into one variant alternative. + virtual bool native_elements(std::vector &) const { return false; } virtual void save(hdf5::archive &) const = 0; virtual void print(std::ostream &) const = 0; virtual void * object(char const * binding) const = 0; @@ -109,6 +113,9 @@ namespace alps { namespace detail { + template struct paramvalue_vector : std::false_type {}; + template struct paramvalue_vector> : std::true_type {}; + template struct paramvalue_serializer : public boost::static_visitor<> { @@ -163,8 +170,19 @@ namespace alps { return *this; } template T cast() const { - if (source_) + if (source_) { + if constexpr (paramvalue_vector::value) { + std::vector elements; + if (source_->native_elements(elements)) { + T values; + values.reserve(elements.size()); + for (auto const & element : elements) + values.push_back(element.cast()); + return values; + } + } return source_->native_value().cast(); + } paramvalue_reader< T > visitor; boost::apply_visitor(visitor, *this); return visitor.get_value(); diff --git a/test/pyalps/native_params/check.py b/test/pyalps/native_params/check.py index 7104f5f9d..fa649fa28 100644 --- a/test/pyalps/native_params/check.py +++ b/test/pyalps/native_params/check.py @@ -42,6 +42,21 @@ parameters["value"] = np.array(2 ** 53 + 1, dtype=np.int64) assert native.wide_integer(parameters) == 2 ** 53 + 1 +# The legacy reader converted each list element to the requested C++ type. +# A homogeneous intermediate vector rejects valid mixed inputs or loses an +# imaginary component before a complex-valued consumer can read it. +for value, real, complex_values, integers in ( + ([True, 2, 3.5], [1., 2., 3.5], [1+0j, 2+0j, 3.5+0j], [1, 2, 3]), + ([1, "2.5", 3.5], [1., 2.5, 3.5], [1+0j, 2.5+0j, 3.5+0j], [1, 2, 3]), + ([True, 2+3j, "4"], [1., 2., 4.], [1+0j, 2+3j, 4+0j], [1, 2, 4]), + ((1, "2", 3), [1., 2., 3.], [1+0j, 2+0j, 3+0j], [1, 2, 3]), + ([2**53+1, 2], [float(2**53+1), 2.], [complex(2**53+1), 2+0j], [2**53+1, 2]), +): + parameters["value"] = value + assert native.vector(parameters) == real + assert native.complex_vector(parameters) == complex_values + assert native.integer_vector(parameters) == integers + with tempfile.TemporaryDirectory() as directory: with hdf5.archive(directory + "/parameters.h5", "w") as archive: archive["value"] = np.array([2.0, 4.0]) diff --git a/test/pyalps/native_params/probe.cpp b/test/pyalps/native_params/probe.cpp index cdc19a7a5..6fbe16393 100644 --- a/test/pyalps/native_params/probe.cpp +++ b/test/pyalps/native_params/probe.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -10,6 +11,8 @@ namespace nb = nanobind; NB_MODULE(parameter_probe, module) { nb::module_::import_("pyalps.ngs"); module.def("vector", [](alps::params const & p) { return p["value"].cast>(); }); + module.def("complex_vector", [](alps::params const & p) { return p["value"].cast>>(); }); + module.def("integer_vector", [](alps::params const & p) { return p["value"].cast>(); }); module.def("integer", [](alps::params const & p) { return p["value"].cast(); }); module.def("wide_integer", [](alps::params const & p) { return p["value"].cast(); }); module.def("clone", [](alps::params const & p) { return alps::params(p); }); diff --git a/test/pyalps/test_binding_surface.py b/test/pyalps/test_binding_surface.py index 836b3b1ea..ce26db910 100644 --- a/test/pyalps/test_binding_surface.py +++ b/test/pyalps/test_binding_surface.py @@ -540,6 +540,8 @@ def fraction_completed(self): def test_mpi_finalization_ownership(): pytest.importorskip("mpi4py") + if any(name in os.environ for name in ("OMPI_COMM_WORLD_SIZE", "PMI_RANK", "PMIX_RANK")): + pytest.skip("standalone MPI initialization subprocesses cannot inherit an active MPI rank") # Boost.MPI finalized only an environment its Python module initialized. # Importing pyalps.mpi after an existing mpi4py user must therefore leave From 005b1655d4bfa40a3504bf27c391cf0667530669 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Fri, 18 Sep 2026 23:00:20 -0400 Subject: [PATCH 47/52] Resolve vector scalar arithmetic explicitly for older GCC --- src/alps/numeric/functional.hpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/alps/numeric/functional.hpp b/src/alps/numeric/functional.hpp index 3ac1b3be8..0a61f814b 100644 --- a/src/alps/numeric/functional.hpp +++ b/src/alps/numeric/functional.hpp @@ -60,6 +60,30 @@ namespace alps { } }; + // Older GCC versions lose the vector/scalar overload in the mixed + // Boost/ALPS using-declarations above. Resolve it explicitly for + // these operations instead of depending on that overload set. + template struct plus, T, std::vector> { + std::vector operator()(std::vector const & x, T const & y) const { + return alps::numeric::operator+(x, y); + } + }; + template struct plus, std::vector> { + std::vector operator()(T const & x, std::vector const & y) const { + return alps::numeric::operator+(x, y); + } + }; + template struct minus, T, std::vector> { + std::vector operator()(std::vector const & x, T const & y) const { + return alps::numeric::operator-(x, y); + } + }; + template struct minus, std::vector> { + std::vector operator()(T const & x, std::vector const & y) const { + return alps::numeric::operator-(x, y); + } + }; + template struct multiplies { R operator()(T const & x, U const & y) const { using boost::numeric::operators::operator*; From cb9cc3e767f3f051190ef7975fd40c2d3ef903b3 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Sat, 19 Sep 2026 00:22:35 -0400 Subject: [PATCH 48/52] Preserve parameter list text and native checkpoint conversions --- bindings/python/pyalps/cpp/dict_to_params.hpp | 16 ++++ bindings/python/pyalps/cpp/ngs/params.cpp | 9 +- src/alps/ngs/detail/paramvalue.hpp | 8 ++ src/alps/ngs/detail/paramvalue_reader.hpp | 2 +- src/alps/ngs/lib/paramvalue.cpp | 96 +++++++++++++++++++ test/ngs/params/external.cpp | 18 ++++ test/pyalps/native_params/check.py | 48 ++++++++++ test/pyalps/native_params/probe.cpp | 7 ++ 8 files changed, 202 insertions(+), 2 deletions(-) diff --git a/bindings/python/pyalps/cpp/dict_to_params.hpp b/bindings/python/pyalps/cpp/dict_to_params.hpp index 310960c56..93bc42d5b 100644 --- a/bindings/python/pyalps/cpp/dict_to_params.hpp +++ b/bindings/python/pyalps/cpp/dict_to_params.hpp @@ -317,6 +317,22 @@ class python_paramvalue_source final : public alps::detail::paramvalue_source { elements.push_back(python_paramvalue_source(items[i], key_).native_value()); return true; } + bool native_text(std::string & text) const override { + nb::gil_scoped_acquire gil; + nb::handle items(value_); + if (!nb::isinstance(items) && !nb::isinstance(items)) + return false; + // The original reader joined Python element strings. Converting a + // mixed sequence to one native vector first both rejects valid text + // parameters and changes representations such as True and 3.0. + text.clear(); + for (std::size_t i = 0; i < nb::len(items); ++i) { + if (i) text += ','; + nb::object item = items[i]; + text += nb::cast(nb::str(item)); + } + return true; + } alps::detail::paramvalue native_value() const override { nb::gil_scoped_acquire gil; // Defer large integers to ALPS' checked text-to-target conversion. diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index abf98bd89..5253b3dae 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -46,6 +46,13 @@ nb::object paramvalue_to_py(alps::detail::paramvalue const & pv) { auto * value = static_cast(pv.source()->object("python")); if (value) return nb::borrow(value); + std::vector elements; + if (pv.source()->native_elements(elements)) { + nb::list result; + for (auto const & element : elements) + result.append(paramvalue_to_py(element)); + return result; + } return paramvalue_to_py(pv.source()->native_value()); } return boost::apply_visitor( @@ -64,7 +71,7 @@ nb::object params_getitem(alps::params & self, nb::object const & key_obj) { nb::object result = paramvalue_to_py(*value); // Materialize native checkpoint values once. Retain the object so later // mutations survive both subsequent Python lookups and C++ conversions. - if (!value->source()) + if (!value->source() || !value->source()->object("python")) pyalps::set_param_value(self, key, result); return result; } diff --git a/src/alps/ngs/detail/paramvalue.hpp b/src/alps/ngs/detail/paramvalue.hpp index 8e8d19226..bd1962b45 100644 --- a/src/alps/ngs/detail/paramvalue.hpp +++ b/src/alps/ngs/detail/paramvalue.hpp @@ -105,6 +105,9 @@ namespace alps { virtual void save(hdf5::archive &) const = 0; virtual void print(std::ostream &) const = 0; virtual void * object(char const * binding) const = 0; + // Some bindings give sequences a specific textual form. Keep + // that conversion separate from a homogeneous numeric snapshot. + virtual bool native_text(std::string &) const { return false; } }; } @@ -171,6 +174,11 @@ namespace alps { } template T cast() const { if (source_) { + if constexpr (std::is_same::value) { + std::string value; + if (source_->native_text(value)) + return value; + } if constexpr (paramvalue_vector::value) { std::vector elements; if (source_->native_elements(elements)) { diff --git a/src/alps/ngs/detail/paramvalue_reader.hpp b/src/alps/ngs/detail/paramvalue_reader.hpp index 5ff38dbd1..d6099054c 100644 --- a/src/alps/ngs/detail/paramvalue_reader.hpp +++ b/src/alps/ngs/detail/paramvalue_reader.hpp @@ -63,7 +63,7 @@ namespace alps { throw std::invalid_argument("only 1 D array are supported in alps::params" + ALPS_STACKTRACE); else if (size[0] != 0) for (U const * it = ptr; it != ptr + size[0]; ++it) - value += (it == ptr ? "," : "") + cast(*it); + value += (it == ptr ? "" : ",") + cast(*it); } std::string value; diff --git a/src/alps/ngs/lib/paramvalue.cpp b/src/alps/ngs/lib/paramvalue.cpp index 447188910..9dce4518b 100644 --- a/src/alps/ngs/lib/paramvalue.cpp +++ b/src/alps/ngs/lib/paramvalue.cpp @@ -23,6 +23,52 @@ namespace alps { paramvalue_source::~paramvalue_source() = default; + namespace { + // Retain scalar types when a Python list checkpoint is resumed by + // a native simulation. Reuse the existing value-provider contract + // so conversion happens in the type requested by the consumer. + class checkpoint_list final : public paramvalue_source { + public: + explicit checkpoint_list(std::vector values) + : values_(std::move(values)) {} + bool native_elements(std::vector & values) const override { + values = values_; + return true; + } + paramvalue native_value() const override { + bool text = false, complex = false, real = false, integer = false; + for (auto const & value : values_) { + text |= value.which() == paramvalue_index::value; + complex |= value.which() == paramvalue_index>::value; + real |= value.which() == paramvalue_index::value; + integer |= value.which() == paramvalue_index::value; + } + if (text) return converted(); + if (complex) return converted>(); + if (real) return converted(); + if (integer) return converted(); + return converted(); + } + void save(hdf5::archive & ar) const override { + if (ar.is_data("")) ar.delete_data(""); + if (ar.is_group("")) ar.delete_group(""); + ar.create_group(""); + for (std::size_t i = 0; i < values_.size(); ++i) + ar[std::to_string(i)] << values_[i]; + } + void print(std::ostream & out) const override { out << native_value(); } + void * object(char const *) const override { return nullptr; } + private: + template std::vector converted() const { + std::vector values; + values.reserve(values_.size()); + for (auto const & value : values_) values.push_back(value.cast()); + return values; + } + std::vector values_; + }; + } + struct paramvalue_saver: public boost::static_visitor<> { paramvalue_saver(hdf5::archive & a) @@ -77,6 +123,56 @@ namespace alps { } void paramvalue::load(hdf5::archive & ar) { + if (ar.is_group("")) { + // The Python archive stores Boolean/mixed lists as numbered + // scalar children. Native-only simulations must be able to + // resume those parameters too, without a Python decoder. + auto children = ar.list_children(""); + if (children.empty()) + throw std::runtime_error("Cannot load an empty parameter group" + ALPS_STACKTRACE); + std::vector values; + values.reserve(children.size()); + for (std::size_t i = 0; i < children.size(); ++i) { + std::string child = std::to_string(i); + if (!ar.is_data(child)) + throw std::runtime_error("Parameter group is not a scalar list" + ALPS_STACKTRACE); + if (ar.is_complex(child) && ar.dimensions(child) < 2) { + std::complex number; + ar[child] >> number; + values.emplace_back(number); + } else if (!ar.is_scalar(child)) { + throw std::runtime_error("Parameter list contains a nonscalar value" + ALPS_STACKTRACE); + } else if (ar.is_datatype(child)) { + // bool and int8 share their HDF5 storage type. Reading + // the byte directly preserves both 0/1 and signed data. + signed char number; + ar[child] >> number; + std::string type; + if (ar.is_attribute(child + "/@__alps_type__")) + ar[child + "/@__alps_type__"] >> type; + if (type == "int8") values.emplace_back(static_cast(number)); + else values.emplace_back(number != 0); + } else if (ar.is_datatype(child) + || ar.is_datatype(child) + || ar.is_datatype(child)) { + double number; + ar[child] >> number; + values.emplace_back(number); + } else if (ar.is_datatype(child)) { + int number; + ar[child] >> number; + values.emplace_back(number); + } else { + // Keep wider integers exact despite the native + // variant's int-sized integer alternative. + std::string value; + ar[child] >> value; + values.emplace_back(value); + } + } + *this = paramvalue(std::make_shared(std::move(values))); + return; + } #define ALPS_NGS_PARAMVALUE_LOAD_HDF5(T) \ { \ T value; \ diff --git a/test/ngs/params/external.cpp b/test/ngs/params/external.cpp index 8c5b5beb3..8a9047b7a 100644 --- a/test/ngs/params/external.cpp +++ b/test/ngs/params/external.cpp @@ -51,4 +51,22 @@ int main() { parameters["wide"].cast(); throw std::runtime_error("overflowing conversion unexpectedly succeeded"); } catch (std::out_of_range const &) {} + + parameters["names"] = std::vector{"Energy", "Stiffness"}; + require(parameters["names"].cast() == "Energy,Stiffness"); + parameters["names"] = std::vector{"", "middle", ""}; + require(parameters["names"].cast() == ",middle,"); + + alps::hdf5::archive archive("param_external_list.h5", "w"); + archive["/list/0"] << true; + archive["/list/1"] << 2; + archive["/list/2"] << 10.5; + archive.set_context("/list"); + alps::detail::paramvalue list; + list.load(archive); + require(list.cast>() == std::vector({1., 2., 10.5})); + require(list.cast>() == std::vector({1, 2, 10})); + list.save(archive); + list.load(archive); + require(list.cast>() == std::vector({1, 2, 10})); } diff --git a/test/pyalps/native_params/check.py b/test/pyalps/native_params/check.py index fa649fa28..1e03a4363 100644 --- a/test/pyalps/native_params/check.py +++ b/test/pyalps/native_params/check.py @@ -57,6 +57,18 @@ assert native.complex_vector(parameters) == complex_values assert native.integer_vector(parameters) == integers +for value, expected in ( + (["Energy", "Stiffness"], "Energy,Stiffness"), + ([1, "two", 3.0], "1,two,3.0"), + ((True, False), "True,False"), + ([None, {"key": 1}], "None,{'key': 1}"), + (["", "middle", ""], ",middle,"), + ([], ""), +): + parameters["value"] = value + assert native.text(parameters) == expected +assert native.native_text() == ",middle," + with tempfile.TemporaryDirectory() as directory: with hdf5.archive(directory + "/parameters.h5", "w") as archive: archive["value"] = np.array([2.0, 4.0]) @@ -67,6 +79,42 @@ clone["value"] *= 2 assert native.vector(clone) == [4.0, 8.0] + # A C++-created params object has no binding-owned checkpoint decoder. + # Boolean/mixed lists use numbered HDF5 groups, which that native reader + # must understand as well. More than ten elements catches lexical order. + for i, values in enumerate(( + [True, False], [True, 2, 3.5], [1, "2", 3.5], + [True] + [j + 0.5 for j in range(1, 12)], + [True, np.int8(-7), np.int64(2**53 + 1)], + )): + with hdf5.archive(directory + f"/native-list-{i}.h5", "w") as archive: + archive["parameters"] = ngs.params({"value": values}) + archive.set_context("/parameters") + native_parameters = native.empty_vectors() + native.load(native_parameters, archive) + assert native.vector(native_parameters) == [float(value) for value in values] + assert native.integer_vector(native_parameters) == [int(value) for value in values] + assert archive.context == "/parameters" + archive.set_context("/resaved") + native.save(native_parameters, archive) + restored = native.empty_vectors() + native.load(restored, archive) + assert native.vector(restored) == [float(value) for value in values] + assert native.integer_vector(restored) == [int(value) for value in values] + assert archive.context == "/resaved" + restored["value"][0] = 7 + assert native.vector(restored)[0] == 7 + + with hdf5.archive(directory + "/native-complex-list.h5", "w") as archive: + archive["parameters"] = ngs.params({"value": [True, 2+3j, 4.]}) + archive.set_context("/parameters") + native_parameters = native.empty_vectors() + native.load(native_parameters, archive) + assert native.complex_vector(native_parameters) == [1+0j, 2+3j, 4+0j] + native.save(native_parameters, archive) + native.load(native_parameters, archive) + assert native.complex_vector(native_parameters) == [1+0j, 2+3j, 4+0j] + # Native destruction on a thread that started without the GIL must release # the Python value safely, including when it owns the final reference. value = np.ones(3) diff --git a/test/pyalps/native_params/probe.cpp b/test/pyalps/native_params/probe.cpp index 6fbe16393..d6dab8840 100644 --- a/test/pyalps/native_params/probe.cpp +++ b/test/pyalps/native_params/probe.cpp @@ -13,6 +13,12 @@ NB_MODULE(parameter_probe, module) { module.def("vector", [](alps::params const & p) { return p["value"].cast>(); }); module.def("complex_vector", [](alps::params const & p) { return p["value"].cast>>(); }); module.def("integer_vector", [](alps::params const & p) { return p["value"].cast>(); }); + module.def("text", [](alps::params const & p) { return p["value"].cast(); }); + module.def("native_text", [] { + alps::params p; + p["value"] = std::vector{"", "middle", ""}; + return p["value"].cast(); + }); module.def("integer", [](alps::params const & p) { return p["value"].cast(); }); module.def("wide_integer", [](alps::params const & p) { return p["value"].cast(); }); module.def("clone", [](alps::params const & p) { return alps::params(p); }); @@ -27,6 +33,7 @@ NB_MODULE(parameter_probe, module) { return p; }); module.def("load", [](alps::params & p, alps::hdf5::archive & ar) { p.load(ar); }); + module.def("save", [](alps::params const & p, alps::hdf5::archive & ar) { p.save(ar); }); module.def("threaded_vector", [](alps::params const & p) { std::vector values; std::exception_ptr error; From 1d9d915b9e86346b53ce1339d2208d2787d4641c Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Sun, 20 Sep 2026 20:55:13 -0400 Subject: [PATCH 49/52] Fix rectangular HDF5 compatibility and preserve validation evidence --- .github/workflows/build_wheels.yml | 10 +- bindings/python/pyalps/cpp/ngs/hdf5.cpp | 54 +- script/pyalps_compatibility/README.md | 134 ++++ script/pyalps_compatibility/applications.py | 96 +++ script/pyalps_compatibility/checkpoint.py | 79 +++ script/pyalps_compatibility/compare.py | 62 ++ .../expected_differences.json | 596 ++++++++++++++++++ .../pyalps_compatibility/legacy-build.patch | 37 ++ script/pyalps_compatibility/probe.py | 332 ++++++++++ script/validate_pyalps.py | 203 ++++++ test/packaging/test_compatibility_report.py | 40 ++ test/pyalps/test_archive_dtypes.py | 55 ++ 12 files changed, 1691 insertions(+), 7 deletions(-) create mode 100644 script/pyalps_compatibility/README.md create mode 100644 script/pyalps_compatibility/applications.py create mode 100644 script/pyalps_compatibility/checkpoint.py create mode 100644 script/pyalps_compatibility/compare.py create mode 100644 script/pyalps_compatibility/expected_differences.json create mode 100644 script/pyalps_compatibility/legacy-build.patch create mode 100644 script/pyalps_compatibility/probe.py create mode 100644 script/validate_pyalps.py create mode 100644 test/packaging/test_compatibility_report.py diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 0427eeb3a..86bf920ef 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -134,8 +134,14 @@ jobs: - name: Import and run binding surface tests run: | - python -c "import pyalps, pyalps.alea, pyalps.hdf5, pyalps.pytools; print(pyalps.__file__)" - python -m pytest -q test/pyalps + python script/validate_pyalps.py --output _build/validation --wheelhouse wheelhouse + + - name: Retain installed-wheel validation evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: pyalps-validation-${{ matrix.plat.os }}-py${{ matrix.python }} + path: _build/validation # The ordinary wheel tests deliberately keep mpi4py optional. This job # installs one consistent Open MPI stack and verifies real inter-rank diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 4ca38b1fe..757426836 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -201,6 +201,7 @@ namespace alps { return; } list_vectorizer v; + std::string stack_dtype; if (v.analyze(l, 0)) { switch (v.kind) { case list_vectorizer::leaf_kind::integral: @@ -225,7 +226,7 @@ namespace alps { case list_vectorizer::leaf_kind::none: break; // e.g. [[], []] → group descent } - } else if (numpy_stackable(l)) { + } else if (numpy_stackable(l, stack_dtype)) { // Legacy vectorized numpy content too: homogeneous // numpy-scalar lists (numpy.int64 etc. were // scalar_types entries) and rectangular trees @@ -237,8 +238,9 @@ namespace alps { // dtypes fall through to the group descent below. nb::object arr; try { - arr = nb::borrow(alps::python::numpy_module()) - .attr("asarray")(l); + arr = alps::python::numpy_module().attr("asarray")( + l, nb::arg("dtype") = stack_dtype.empty() + ? nb::none() : nb::cast(stack_dtype)); } catch (nb::python_error &) { arr = nb::object(); } @@ -290,12 +292,43 @@ namespace alps { bool has_bool_leaf = false; bool homogeneous_numpy_scalars = true; PyTypeObject * numpy_scalar_type = nullptr; + bool homogeneous_storage = true; + bool homogeneous_scalar_rows = true; + std::string storage_dtype; + void inspect_storage(nb::handle value) { + std::string dtype; + PyObject * raw = value.ptr(); + if (is_ndarray(raw) || is_numpy_scalar(raw)) + dtype = nb::cast(value.attr("dtype").attr("name")); + else if (PyLong_CheckExact(raw)) { + int overflow = 0; + long long number = PyLong_AsLongLongAndOverflow(raw, &overflow); + if (!overflow) + dtype = number >= std::numeric_limits::min() + && number <= std::numeric_limits::max() + ? "int32" : "int64"; + } else if (PyFloat_CheckExact(raw)) + dtype = "float64"; + else if (PyComplex_CheckExact(raw)) + dtype = "complex128"; + if (dtype.empty() || (!storage_dtype.empty() && storage_dtype != dtype)) + homogeneous_storage = false; + if (storage_dtype.empty()) storage_dtype = std::move(dtype); + } }; static void scan_tree(nb::handle node, tree_scan & scan) { std::size_t const n = nb::len(node); + PyTypeObject * row_scalar_type = nullptr; for (std::size_t i = 0; i < n; ++i) { nb::object item = node[i]; PyObject * raw = item.ptr(); + if (!PyList_Check(raw) && !PyTuple_Check(raw)) + scan.inspect_storage(item); + if (!PyList_Check(raw) && !PyTuple_Check(raw) && !is_ndarray(raw)) { + if (row_scalar_type && row_scalar_type != Py_TYPE(raw)) + scan.homogeneous_scalar_rows = false; + row_scalar_type = Py_TYPE(raw); + } if (is_ndarray(raw)) { scan.has_ndarray = true; } else if (PyList_Check(raw) || PyTuple_Check(raw)) { @@ -318,14 +351,15 @@ namespace alps { } // The list shapes the legacy build stacked into one // dataset beyond plain scalars: (a) numpy scalars of ONE - // type (exact tp_name match, like legacy scalar_types), or + // type (exact tp_name match, like legacy scalar_types), compatible + // Python/NumPy rows with the same ALPS storage dtype, or // (b) sequences/ndarrays only, with an ndarray somewhere in // the tree (legacy vectorized extent-matched mixes of // list/tuple/ndarray nodes) — but never when a plain bool // sits among the leaves, which numpy would silently promote // to 0/1. Pure-list trees never reach (b) — their // exact-type handling stays with list_vectorizer. - static bool numpy_stackable(nb::list const & l) { + static bool numpy_stackable(nb::list const & l, std::string & dtype) { char const * first_scalar = nullptr; bool scalars_only = true; bool sequences_only = true; @@ -350,6 +384,16 @@ namespace alps { return false; tree_scan scan; scan_tree(l, scan); + // ALPS writes ordinary Python integers as int32 when they + // fit. Letting NumPy infer the dtype of the entire tree + // widens [[1, 2], array([3, 4], dtype=int32)] to int64. + // Select the shared storage dtype only when every leaf + // agrees, so this never narrows a wider integer/array. + if (scan.homogeneous_storage) dtype = scan.storage_dtype; + if (scan.has_numpy_scalar && scan.has_other_scalar + && scan.homogeneous_storage && scan.homogeneous_scalar_rows + && !scan.has_bool_leaf) + return true; // A rectangular tree made solely from one exact NumPy // scalar type is vectorizable at any nesting depth. // np.asarray below performs the final rectangularity diff --git a/script/pyalps_compatibility/README.md b/script/pyalps_compatibility/README.md new file mode 100644 index 000000000..bb22c957f --- /dev/null +++ b/script/pyalps_compatibility/README.md @@ -0,0 +1,134 @@ +# Reproducing the Python migration evidence + +These checks use native Python environments and the existing ALPS SDK. They +do not need containers, a virtual machine, or a new CI build matrix. The normal +wheel smoke jobs run the same binding suite as before and upload its evidence. +Legacy comparisons and the six solver workflows are opt-in local checks. + +## Installed-wheel validation + +Install a wheel in an environment containing NumPy, SciPy, and pytest. From +the repository root run: + +```sh +python script/validate_pyalps.py --output _build/validation --wheelhouse wheelhouse +``` + +The output contains a JUnit report, test log, source revision and dirty-file +hashes, installed package/dependency versions, extension hashes, distribution +hashes, executed commands, timings, and pass/fail status. A failing check exits +nonzero and retains its evidence. Each subprocess has a five-minute timeout. +Outputs belong in an ignored build directory or outside the checkout. + +Use `--packaging` to include release/packaging tests (requires Python 3.11 or +newer, packaging, and scikit-build-core). Use `--downstream` to enable the two compiled consumers; +this requires the matching SDK installed at `_build/wheel-deps/install`, a +C++ compiler, CMake, and nanobind. Check the reported skips: a standard wheel +smoke run does not exercise these opt-in consumers or MPI without mpi4py. + +Use `--applications` for input generation, dispatch, and result loading through +the installed `spinmc`, `loop`, `dirloop_sse`, `sparsediag`, `fulldiag`, and +`dmrg` programs. The exact-diagonalization cases assert the four-site periodic +Heisenberg ground-state energy, -2, within 1e-10. The Monte Carlo and short +DMRG cases assert finite loaded results; they do not establish convergence. +Each run writes its own inputs and outputs under the evidence directory. + +## Old/new behavioral comparison + +The reference is master at `f28d428017773f5794dc9896a544f95e8ef40443`. +Build its Boost.Python modules in a separate checkout with the **same Python +and NumPy versions** as the new-wheel environment. NumPy 1.26 and Python 3.10 +are a useful common baseline. Keep old and new modules in separate processes. + +`legacy-build.patch` records the build-only adaptations used in the original +audit: expose the experimental accumulator module without selecting the +unrelated global new-ALEA mode, and link Python modules through `Python::Module` +without standalone Python's redundant extra libraries. It does not patch the +compared binding algorithms. Apply it only in the legacy checkout. + +For example, with `LEGACY_SOURCE`, `LEGACY_BUILD`, `LEGACY_PYTHON`, and +`PR_SOURCE` set to your paths: + +```sh +git -C "$LEGACY_SOURCE" checkout f28d428017773f5794dc9896a544f95e8ef40443 +git -C "$LEGACY_SOURCE" apply "$PR_SOURCE/script/pyalps_compatibility/legacy-build.patch" +cmake -S "$LEGACY_SOURCE" -B "$LEGACY_BUILD" \ + -DALPS_BUILD_PYTHON=ON -DALPS_BUILD_APPLICATIONS=OFF \ + -DALPS_BUILD_TESTS=OFF -DALPS_BUILD_EXAMPLES=OFF \ + -DALPS_BUILD_DEVELOPER_TOOLS=OFF -DALPS_BUILD_FORTRAN=OFF \ + -DALPS_ENABLE_MPI=OFF -DALPS_ENABLE_OPENMP=OFF \ + -DALPS_NGS_USE_NEW_ALEA=OFF -DPython_EXECUTABLE="$LEGACY_PYTHON" +cmake --build "$LEGACY_BUILD" --parallel 2 +``` + +Use your normal HDF5 and Boost configuration. The original audit used Boost +1.87, Apple Clang, and a package-specific SZIP include directory to avoid +mixing Homebrew Boost headers with the selected Boost sources. This baseline +build is a one-time local prerequisite, not an additional CI job. + +Then run from the PR checkout, using the new-wheel environment's Python: + +```sh +python script/validate_pyalps.py --output _build/compatibility \ + --legacy-python "$LEGACY_PYTHON" \ + --legacy-modules "$LEGACY_BUILD/lib/pyalps" +``` + +`probe.py` records public names, class members, scalar/vector arithmetic, +observables, timeseries, RNG sequences, HDF5 types/shapes/layouts, and parameter +values. It includes the three rectangular-table cases found in the adversarial +review. The original corpus had 403 records; this corpus has 406. Records +include inventories and probes, not 406 independent numerical tests. + +`compare.py` rejects mismatched Python/NumPy versions and unexpected changes. +`expected_differences.json` contains the exact old and new values and reasons +for the 19 previously examined differences. It is not a name-only allowlist: +a different value under an allowed name still fails. A disappearing expected +difference also fails and must be examined before updating the file. + +The numeric fingerprints round floating values to 11 significant digits, as +in the original corpus, to suppress insignificant floating-point noise. This +is not a universal scientific tolerance or a substitute for solver-specific +reference tests. Arrays retain dtype and shape; selected parameter probes also +record their Python type. The ordinary tests assert arithmetic and ownership +contracts that a normalized fingerprint alone cannot establish. + +The runner also writes checkpoints using each implementation and reads them +with the other. These cover ordinary parameters, RNG continuation, scalar and +vector observations, and persisted results. Legacy `result.load()` is broken; +the legacy side checks the stored result datasets, while the new side performs +actual result restoration. This does not claim compatibility for arbitrary +metadata or all native parameter dtypes. + +## Local sdist rebuild and additional checks + +Build the matching native SDK using the documented `wheel-deps` preset. Set +`ALPS_DIR` to its installed `share/alps` directory. Then, outside the checkout: + +```sh +python -m build --sdist --outdir dist "$PR_SOURCE/bindings/python/pyalps" +mkdir extracted +tar -xzf dist/pyalps-*.tar.gz -C extracted +python -m build --wheel --outdir wheelhouse extracted/pyalps-* +``` + +Repair the wheel with the normal platform tool (delocate on macOS, auditwheel +on Linux), install it in a fresh environment, and run `validate_pyalps.py` +against that environment with `--wheelhouse` pointing at the repaired wheel. +This is deliberately a local release check: the normal CI sdist job checks +metadata and payload, while the existing wheel jobs already perform full builds. + +MPI remains covered by the existing CI job. A local two-rank run is: + +```sh +mpiexec -n 2 python -m pytest -q \ + test/pyalps/test_binding_surface.py::test_mpi4py_compatibility_surface \ + test/pyalps/test_mpi_requests.py +``` + +The native-only parameter loader still has an inherited limitation: unsupported +numeric storage types such as a standalone int64 or float32 dataset can leave +the default value at zero. Python-owned parameter loading uses a different +decoder. The migration evidence must not be read as a blanket claim that every +native checkpoint dtype is handled. This issue is separate from the repaired +rectangular-table regression. diff --git a/script/pyalps_compatibility/applications.py b/script/pyalps_compatibility/applications.py new file mode 100644 index 000000000..4845b7559 --- /dev/null +++ b/script/pyalps_compatibility/applications.py @@ -0,0 +1,96 @@ +import json +import os +import pathlib +import sys +import tempfile +import numpy as np +import pyalps + +root = pathlib.Path(os.environ["PYALPS_WORKFLOW_ROOT"]) +root.mkdir(parents=True, exist_ok=True) +app = sys.argv[1] +folder = pathlib.Path(tempfile.mkdtemp(prefix=f"{app}-", dir=root)) +os.chdir(folder) +common = { + "LATTICE": "chain lattice", + "MODEL": "spin", + "local_S": 0.5, + "L": 4, + "J": 1, + "SEED": 42, +} +monte = {**common, "T": 1.0, "THERMALIZATION": 32, "SWEEPS": 256} +cases = { + "loop": {**monte, "ALGORITHM": "loop"}, + # A short smoke run must not depend on thermalization growing the default + # ten-slot operator string before measurement begins. + "dirloop_sse": {**monte, "INITIAL_CUTOFF": 128}, + "spinmc": { + "LATTICE": "square lattice", + "MODEL": "Ising", + "L": 4, + "J": 1, + "T": 2.0, + "UPDATE": "cluster", + "THERMALIZATION": 32, + "SWEEPS": 256, + "SEED": 42, + }, + "sparsediag": { + **common, + "CONSERVED_QUANTUMNUMBERS": "Sz", + "Sz_total": 0, + "NUMBER_EIGENVALUES": 2, + }, + "fulldiag": { + **common, + "CONSERVED_QUANTUMNUMBERS": "Sz", + "T_MIN": 0.5, + "T_MAX": 1.5, + "DELTA_T": 0.5, + }, + "dmrg": { + **common, + "LATTICE": "open chain lattice", + "CONSERVED_QUANTUMNUMBERS": "N,Sz", + "Sz_total": 0, + "SWEEPS": 2, + "MAXSTATES": 16, + "NUMBER_EIGENVALUES": 1, + }, +} +input_file = pyalps.writeInputFiles("test", [cases[app]]) +status = pyalps.runApplication(app, input_file, Tmin=1, T=45, writexml=True) +assert status[0] == 0, (app, status) +files = pyalps.getResultFiles(prefix="test") +assert files +if app in ("loop", "spinmc", "dirloop_sse"): + data = pyalps.loadMeasurements(files, ["Energy"]) +elif app in ("sparsediag", "fulldiag"): + data = pyalps.loadSpectra(files) +else: + data = pyalps.loadEigenstateMeasurements(files) +values = [] +for dataset in pyalps.flatten(data): + for item in np.asarray(dataset.y, dtype=object).flat: + if isinstance(item, np.ndarray): + values.extend(np.ravel(item).tolist()) + elif hasattr(item, "mean") and not callable(item.mean): + values.append(item.mean) + else: + values.append(item) +assert values, (app, "no loaded data") +assert np.isfinite(np.asarray(values, dtype=float)).all(), (app, values) +if app in ("sparsediag", "fulldiag"): + np.testing.assert_allclose(min(values), -2.0, rtol=0, atol=1e-10) +print( + json.dumps( + { + "application": app, + "files": len(files), + "loaded_finite_values": len(values), + "sample": list(map(float, values[:4])), + } + ), + flush=True, +) diff --git a/script/pyalps_compatibility/checkpoint.py b/script/pyalps_compatibility/checkpoint.py new file mode 100644 index 000000000..2f8614fba --- /dev/null +++ b/script/pyalps_compatibility/checkpoint.py @@ -0,0 +1,79 @@ +import copy +import importlib +import sys +import numpy as np + +legacy, action, filename = sys.argv[1] == "legacy", sys.argv[2], sys.argv[3] + + +def module(n): + return importlib.import_module(n if legacy else "pyalps.cxx." + n) + + +module("pyalea_c") +d = module("pymcdata_c") +h = module("pyngshdf5_c") +p = module("pyngsparams_c") +r = module("pyngsrandom01_c") +o = module("pyngsobservables_c") +module("pyngsobservable_c") +results = module("pyngsresult_c") +ar = h.hdf5_archive_impl(filename, "w" if action == "write" else "r") +if action == "write": + params = p.params( + { + "L": 16, + "T": 1.25, + "SEED": 42, + "label": "check Ω", + "couplings": np.array([1.0, 2.0, 3.0]), + } + ) + rng = r.random01(42) + for _ in range(7): + rng() + expected_rng = copy.deepcopy(rng) + expected = [expected_rng() for _ in range(8)] + observables = o.observables() + observables.createRealObservable("Energy") + observables.createRealVectorObservable("Correlations") + for i in range(64): + observables["Energy"].append(float(i)) + observables["Correlations"].append(np.array([float(i), 2.0 * i])) + result = results.observable2result(observables["Energy"]) + ar["/expected_rng"] = np.array(expected) + for path, value in [ + ("parameters", params), + ("rng", rng), + ("observables", observables), + ("result", result), + ("vector_result", results.observable2result(observables["Correlations"])), + ]: + ar.set_context("/" + path) + value.save(ar) +else: + params = p.params(ar, "/parameters") + assert params["L"] == 16 and params["T"] == 1.25 and params["label"] == "check Ω" + np.testing.assert_array_equal(ar["/parameters/couplings"], [1, 2, 3]) + rng = r.random01() + ar.set_context("/rng") + rng.load(ar) + np.testing.assert_array_equal([rng() for _ in range(8)], ar["/expected_rng"]) + observables = o.observables() + observables.load(ar, "/observables") + for name, mean in [("Energy", 31.5), ("Correlations", [31.5, 63.0])]: + result = results.observable2result(observables[name]) + np.testing.assert_allclose(result.mean, mean) + assert result.count == 64 + # Legacy result.load itself is broken. Inspect its persisted values, and + # exercise actual scalar/vector restoration through the corrected binding. + assert ar["/result/mean/value"] == 31.5 and ar["/result/count"] == 64 + if not legacy: + for path, mean in [("/result", 31.5), ("/vector_result", [31.5, 63.0])]: + restored = results.result() + ar.set_context(path) + restored.load(ar) + np.testing.assert_allclose(restored.mean, mean) + assert restored.count == 64 + print(("legacy" if legacy else "nanobind") + " read checkpoint: OK") +ar.close() diff --git a/script/pyalps_compatibility/compare.py b/script/pyalps_compatibility/compare.py new file mode 100644 index 000000000..3336aceab --- /dev/null +++ b/script/pyalps_compatibility/compare.py @@ -0,0 +1,62 @@ +"""Compare independently collected records, failing on unclassified changes.""" + +import argparse +import json +from pathlib import Path + + +def compare(legacy, candidate, expected): + for key in ("python", "numpy"): + if legacy["environment"][key] != candidate["environment"][key]: + raise ValueError( + f"Compare matching {key} versions, not different environments" + ) + before, after = legacy["records"], candidate["records"] + differences = { + name: {"legacy": before.get(name), "nanobind": after.get(name)} + for name in sorted(before.keys() | after.keys()) + if before.get(name) != after.get(name) + } + classified = { + name: {key: entry[key] for key in ("legacy", "nanobind")} + for name, entry in expected.items() + } + unexpected = { + name: entry + for name, entry in differences.items() + if classified.get(name) != entry + } + stale = sorted(classified.keys() - differences.keys()) + return { + "records": len(before.keys() | after.keys()), + "identical": len(before.keys() | after.keys()) - len(differences), + "classified_differences": len(differences) - len(unexpected), + "unexpected": unexpected, + "stale_classifications": stale, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("legacy", type=Path) + parser.add_argument("candidate", type=Path) + parser.add_argument("output", type=Path) + parser.add_argument( + "--expected", + type=Path, + default=Path(__file__).with_name("expected_differences.json"), + ) + args = parser.parse_args() + result = compare( + *( + json.loads(path.read_text()) + for path in (args.legacy, args.candidate, args.expected) + ) + ) + args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(json.dumps(result, sort_keys=True)) + return bool(result["unexpected"] or result["stale_classifications"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/script/pyalps_compatibility/expected_differences.json b/script/pyalps_compatibility/expected_differences.json new file mode 100644 index 000000000..633551850 --- /dev/null +++ b/script/pyalps_compatibility/expected_differences.json @@ -0,0 +1,596 @@ +{ + "MCVectorData/full": { + "legacy": { + "ok": { + "error": { + "array": [], + "dtype": "" + }, + "nanobind": { + "ok": { + "array": [ + 1, + 1 + ], + "dtype": "" + }, + "nanobind": { + "ok": { + "array": [ + 1, + 1 + ], + "dtype": " 1 + else data.copy() + ) + if layout == "readonly": + value.flags.writeable = False + probe( + "hdf/array/" + dtype + "/" + str(shape) + "/" + layout, + lambda value=value: roundtrip(value), + ) + for label, value in [ + ("list", [1, 2, 3]), + ("nested", [[1.0, 2.0], [3.0, 4.0]]), + ("ragged", [[1], [2, 3]]), + ("mixed", [1, "two", 3.0]), + ("dictionary", {"a": [1, 2], "b": {"c": 1.5}}), + ("empty-list", []), + ("empty-dict", {}), + ("complex-list", [1 + 2j, 3 + 4j]), + ]: + probe("hdf/container/" + label, lambda value=value: roundtrip(value)) + for label, value in [ + ("python-numpy-integer-rows", [[1, 2], [np.int32(3), np.int32(4)]]), + ("python-numpy-float-rows", [[1.0, 2.0], [np.float64(3.0), np.float64(4.0)]]), + ("list-ndarray-dtype", [[1, 2], np.array([3, 4], dtype=np.int32)]), + ]: + probe("hdf/container/" + label, lambda value=value: roundtrip(value)) + archive.close() + +for label, value in [ + ("int", 7), + ("large-int", 2**40), + ("float", 1.25), + ("bool", True), + ("str", "a"), + ("complex", 1 + 2j), + ("list", [1.0, 2.0]), + ("array", np.array([1.0, 2.0])), + ("matrix", np.ones((2, 2))), + ("None", None), + ("dict", {"x": 1}), + ("tuple", (1, 2)), + ("numpy-scalar", np.int64(7)), +]: + + def params_probe(value=value): + par = p.params({"x": value}) + got = par["x"] + return {"type": type(got).__name__, "value": normalize(got)} + + probe("params/" + label, params_probe) +modules = {} +for name in ("pyalea_c", "pymcdata_c", "pyngshdf5_c", "pyngsparams_c"): + filename = module(name).__file__ + with open(filename, "rb") as source: + modules[name] = { + "path": filename, + "sha256": hashlib.sha256(source.read()).hexdigest(), + } +environment = { + "python": platform.python_version(), + "numpy": np.__version__, + "platform": platform.platform(), + "modules": modules, +} +with open(sys.argv[2], "w") as output: + json.dump( + {"environment": environment, "records": results}, + output, + indent=2, + sort_keys=True, + ) +print( + len(results), + "probes", + len([v for v in results.values() if "error" in v]), + "exceptions", +) diff --git a/script/validate_pyalps.py b/script/validate_pyalps.py new file mode 100644 index 000000000..7d5ed941e --- /dev/null +++ b/script/validate_pyalps.py @@ -0,0 +1,203 @@ +"""Run installed-pyalps checks and retain reproducible evidence; no SDK build.""" + +import argparse +import hashlib +import importlib.metadata +import json +import os +from pathlib import Path +import platform +import subprocess +import sys +import time +import xml.etree.ElementTree as ET + + +ROOT = Path(__file__).resolve().parents[1] + + +def sha256(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def git(*args): + return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--wheelhouse", type=Path) + parser.add_argument("--packaging", action="store_true") + parser.add_argument("--downstream", action="store_true") + parser.add_argument("--applications", action="store_true") + parser.add_argument("--legacy-python", type=Path) + parser.add_argument("--legacy-modules", type=Path) + args = parser.parse_args() + if bool(args.legacy_python) != bool(args.legacy_modules): + parser.error("--legacy-python and --legacy-modules must be supplied together") + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + + import pyalps + + package = Path(pyalps.__file__).resolve().parent + environment = os.environ.copy() + if args.downstream: + environment["PYALPS_TEST_DOWNSTREAM_EXPORT"] = "1" + manifest = { + "source_revision": git("rev-parse", "HEAD"), + "source_tree": git("rev-parse", "HEAD^{tree}"), + "source_status": git("status", "--short"), + "dirty_file_hashes": { + name: sha256(ROOT / name) if (ROOT / name).is_file() else None + for name in git( + "ls-files", "--modified", "--others", "--exclude-standard" + ).splitlines() + }, + "python": sys.version, + "platform": platform.platform(), + "packages": dict( + sorted( + (d.metadata["Name"], d.version) + for d in importlib.metadata.distributions() + ) + ), + "installed_package": str(package), + "installed_extension_hashes": { + str(path.relative_to(package)): sha256(path) + for path in sorted(package.rglob("*.so")) + }, + "downstream_enabled": environment.get("PYALPS_TEST_DOWNSTREAM_EXPORT") == "1", + "build_environment": { + name: os.environ[name] + for name in ("CPLUS_INCLUDE_PATH", "ALPS_DIR", "CMAKE_ARGS") + if name in os.environ + }, + "steps": [], + } + if args.wheelhouse: + manifest["distribution_hashes"] = { + path.name: sha256(path) + for path in sorted(args.wheelhouse.iterdir()) + if path.name.endswith((".whl", ".tar.gz")) + } + (output / "source.patch").write_text(git("diff", "HEAD") + "\n") + + def run(name, command, env=None): + command = [str(arg) for arg in command] + print(f"Running {name}", flush=True) + start = time.monotonic() + with (output / f"{name}.log").open("w") as log: + try: + result = subprocess.run( + command, + cwd=ROOT, + env=env or environment, + stdout=log, + stderr=subprocess.STDOUT, + timeout=300, + ) + code = result.returncode + except subprocess.TimeoutExpired: + code = 124 + manifest["steps"].append( + { + "name": name, + "command": command, + "returncode": code, + "seconds": round(time.monotonic() - start, 2), + } + ) + if code: + raise RuntimeError( + f"{name} failed ({code}); see {output / (name + '.log')}" + ) + + success = False + try: + tests = ["test/pyalps"] + (["test/packaging"] if args.packaging else []) + run( + "pytest", + [ + sys.executable, + "-m", + "pytest", + "-q", + *tests, + "--junitxml", + output / "pytest.xml", + ], + ) + scripts = ROOT / "script" / "pyalps_compatibility" + if args.legacy_python: + old_env = {**environment, "PYTHONPATH": str(args.legacy_modules.resolve())} + old_python = args.legacy_python.absolute() + run( + "legacy-probes", + [old_python, scripts / "probe.py", "legacy", output / "legacy.json"], + old_env, + ) + run( + "candidate-probes", + [ + sys.executable, + scripts / "probe.py", + "nanobind", + output / "candidate.json", + ], + ) + run( + "comparison", + [ + sys.executable, + scripts / "compare.py", + output / "legacy.json", + output / "candidate.json", + output / "comparison.json", + ], + ) + for writer, reader in (("legacy", "nanobind"), ("nanobind", "legacy")): + for mode, action in ((writer, "write"), (reader, "read")): + run( + f"checkpoint-{writer}-{action}", + [ + old_python if mode == "legacy" else sys.executable, + scripts / "checkpoint.py", + mode, + action, + output / f"{writer}.h5", + ], + old_env if mode == "legacy" else environment, + ) + if args.applications: + for app in ( + "spinmc", + "loop", + "dirloop_sse", + "sparsediag", + "fulldiag", + "dmrg", + ): + run( + app, + [sys.executable, scripts / "applications.py", app], + { + **environment, + "PYALPS_WORKFLOW_ROOT": str(output / "applications"), + }, + ) + success = True + finally: + manifest["success"] = success + if (output / "pytest.xml").exists(): + manifest["test_suites"] = [ + suite.attrib + for suite in ET.parse(output / "pytest.xml").iter("testsuite") + ] + (output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + print(f"Validation passed; evidence: {output}") + + +if __name__ == "__main__": + main() diff --git a/test/packaging/test_compatibility_report.py b/test/packaging/test_compatibility_report.py new file mode 100644 index 000000000..fc16eadd1 --- /dev/null +++ b/test/packaging/test_compatibility_report.py @@ -0,0 +1,40 @@ +"""An expected difference must not hide a different or newly missing result.""" + +import importlib.util +from pathlib import Path + +import pytest + +path = Path(__file__).resolve().parents[2] / "script/pyalps_compatibility/compare.py" +spec = importlib.util.spec_from_file_location("compatibility_compare", path) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +def report(records, numpy="1.26.4"): + return {"environment": {"python": "3.10.20", "numpy": numpy}, "records": records} + + +@pytest.mark.parametrize("candidate", [{"fixed": 3, "stable": 7}, {"fixed": 2}]) +def test_classification_does_not_hide_changed_or_missing_values(candidate): + expected = {"fixed": {"legacy": 1, "nanobind": 2, "reason": "known fix"}} + result = module.compare( + report({"fixed": 1, "stable": 7}), report(candidate), expected + ) + assert result["unexpected"] + + +def test_comparison_accepts_only_the_classified_values(): + expected = {"fixed": {"legacy": 1, "nanobind": 2, "reason": "known fix"}} + result = module.compare( + report({"fixed": 1, "stable": 7}), report({"fixed": 2, "stable": 7}), expected + ) + assert result["identical"] == 1 and result["classified_differences"] == 1 + assert not result["unexpected"] and not result["stale_classifications"] + result = module.compare(report({"fixed": 2}), report({"fixed": 2}), expected) + assert result["stale_classifications"] == ["fixed"] + + +def test_comparison_rejects_different_dependencies(): + with pytest.raises(ValueError, match="matching numpy"): + module.compare(report({}), report({}, numpy="2.0.0"), {}) diff --git a/test/pyalps/test_archive_dtypes.py b/test/pyalps/test_archive_dtypes.py index 129b02073..ff0b60529 100644 --- a/test/pyalps/test_archive_dtypes.py +++ b/test/pyalps/test_archive_dtypes.py @@ -6,6 +6,61 @@ from pyalps import hdf5, ngs +@pytest.mark.parametrize("depth", [2, 3]) +@pytest.mark.parametrize("reverse", [False, True]) +@pytest.mark.parametrize( + "second_row,dtype", + [ + ([np.int32(3), np.int32(4)], np.int32), + ([np.float64(3), np.float64(4)], np.float64), + ([np.complex128(3), np.complex128(4)], np.complex128), + (np.array([3, 4], dtype=np.int32), np.int32), + ], +) +def test_rectangular_python_numpy_rows_keep_array_contract( + tmp_path, second_row, dtype, reverse, depth +): + # Both rows have the same ALPS storage dtype. Plain Python integers use + # int32, even when NumPy's platform-default integer is int64. + scalar = {np.int32: int, np.float64: float, np.complex128: complex}[dtype] + value = [[scalar(1), scalar(2)], second_row] + if reverse: + value.reverse() + if depth == 3: + value = [value, value] + expected = np.asarray(value, dtype=dtype) + filename = str(tmp_path / "rectangular.h5") + with hdf5.archive(filename, "w") as archive: + archive["table"] = value + with hdf5.archive(filename, "r") as archive: + restored = archive["table"] + assert archive.is_data("table") + # Value-only NumPy comparisons coerce lists and miss changed arithmetic. + assert isinstance(restored, np.ndarray) + assert restored.dtype == expected.dtype and restored.shape == expected.shape + np.testing.assert_array_equal(restored[..., 0], expected[..., 0]) + np.testing.assert_array_equal(restored * 2, expected * 2) + + +@pytest.mark.parametrize( + "value", + [ + [[1, np.int32(2)], [np.int32(3), np.int32(4)]], + [[1, 2], [np.int32(3)]], + [[True, False], [np.int32(3), np.int32(4)]], + [[2**53 + 1, 2**53 + 3], [np.int32(3), np.int32(4)]], + ], +) +def test_incompatible_python_numpy_rows_keep_groups(tmp_path, value): + with hdf5.archive(str(tmp_path / "groups.h5"), "w") as archive: + archive["table"] = value + assert archive.is_group("table") + restored = archive["table"] + assert isinstance(restored, list) + for actual, expected in zip(restored, value): + assert list(actual) == list(expected) + + @pytest.mark.parametrize("dtype", [np.bool_, np.int8]) @pytest.mark.parametrize("shape", [(), (3,), (2, 3), (2, 1, 3), (0,), (2, 0)]) @pytest.mark.parametrize("attribute", [False, True]) From d590614e7388a319793414d09ac8aa985fff4c5a Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Sun, 20 Sep 2026 22:22:05 -0400 Subject: [PATCH 50/52] Preserve exact integers when stacking mixed HDF5 rows --- bindings/python/pyalps/README.md | 7 ++ bindings/python/pyalps/cpp/ngs/hdf5.cpp | 55 +++++++++++-- script/pyalps_compatibility/README.md | 8 ++ test/pyalps/test_archive_dtypes.py | 104 ++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index b2f72b4fe..15626aed0 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -88,6 +88,13 @@ interpretation. The old format cannot distinguish an unmarked `int8` array from a Boolean mask; use a typed reader such as h5py when an old dataset is known to contain signed bytes. +Rectangular mixtures of numeric rows are stored as a single array when every +integer remains exact in the common dtype. If mixing integer widths or mixing +integers with floating-point or complex rows would round a value, the archive +stores the rows separately and reads them back as a list. For example, a +`uint64` row containing `2**63 + 1` alongside an `int64` row retains its exact +integer values instead of silently converting them to `float64`. + `pyalps.mpi` receives Python objects using matched probes, so asynchronous receives and the wait/test helpers can handle messages larger than mpi4py's default object receive buffer. This adapter exchanges mpi4py messages; diff --git a/bindings/python/pyalps/cpp/ngs/hdf5.cpp b/bindings/python/pyalps/cpp/ngs/hdf5.cpp index 757426836..64688beeb 100644 --- a/bindings/python/pyalps/cpp/ngs/hdf5.cpp +++ b/bindings/python/pyalps/cpp/ngs/hdf5.cpp @@ -234,8 +234,9 @@ namespace alps { // one dataset. Delegate to numpy so shape checking // and dtype handling match numpy's rules, then feed // the stacked array through the ndarray save path. - // Ragged shapes (numpy raises) and non-numeric - // dtypes fall through to the group descent below. + // Ragged shapes (numpy raises), non-numeric dtypes, + // and lossy integer promotions fall through to the + // group descent below. nb::object arr; try { arr = alps::python::numpy_module().attr("asarray")( @@ -249,9 +250,20 @@ namespace alps { nb::cast(arr.attr("dtype").attr("kind")); if (dtype_kind.size() == 1 && std::strchr("biufc", dtype_kind[0])) { - hdf5_save_py11_visitor child_visitor{ar, path}; - extract_from_pyobject_py11(child_visitor, arr); - return; + bool lossless = true; + if (stack_dtype.empty() + && std::strchr("fc", dtype_kind[0])) { + nb::object dtype = arr.attr("dtype"); + int precision = nb::cast( + alps::python::numpy_module().attr("finfo")( + dtype).attr("nmant")) + 1; + lossless = integers_preserved(l, dtype, precision); + } + if (lossless) { + hdf5_save_py11_visitor child_visitor{ar, path}; + extract_from_pyobject_py11(child_visitor, arr); + return; + } } } } @@ -285,6 +297,39 @@ namespace alps { return true; return false; } + // NumPy promotes int64/uint64 mixtures to float64, and also + // mixes wide integers with float/complex rows. Its numeric + // equality can report rounded integers as equal after that + // same promotion. Compare Python objects instead, and only + // inspect integer types wider than the target's significand. + static bool integers_preserved(nb::handle node, nb::handle dtype, + int precision) { + PyObject * raw = node.ptr(); + if (PyList_Check(raw) || PyTuple_Check(raw)) { + for (std::size_t i = 0; i < nb::len(node); ++i) + if (!integers_preserved(node[i], dtype, precision)) + return false; + return true; + } + if (is_ndarray(raw) || is_numpy_scalar(raw)) { + nb::object source_dtype = node.attr("dtype"); + std::string kind = nb::cast(source_dtype.attr("kind")); + if (kind != "i" && kind != "u") return true; + int bits = 8 * nb::cast(source_dtype.attr("itemsize")) + - (kind == "i" ? 1 : 0); + if (bits <= precision) return true; + } else if (!PyLong_Check(raw)) { + return true; + } + nb::handle np = alps::python::numpy_module(); + // astype(object) unboxes NumPy integer scalars to Python + // ints; asarray(scalar, dtype=object) can retain the NumPy + // scalar and its lossy mixed-type comparison semantics. + nb::object original = np.attr("asarray")(node).attr("astype")("object"); + nb::object converted = np.attr("asarray")( + node, nb::arg("dtype") = dtype).attr("astype")("object"); + return nb::cast(np.attr("array_equal")(original, converted)); + } struct tree_scan { bool has_ndarray = false; bool has_numpy_scalar = false; diff --git a/script/pyalps_compatibility/README.md b/script/pyalps_compatibility/README.md index bb22c957f..5df35d1d4 100644 --- a/script/pyalps_compatibility/README.md +++ b/script/pyalps_compatibility/README.md @@ -93,6 +93,14 @@ reference tests. Arrays retain dtype and shape; selected parameter probes also record their Python type. The ordinary tests assert arithmetic and ownership contracts that a normalized fingerprint alone cannot establish. +`test/pyalps/test_archive_dtypes.py` also checks mixed numeric rows at the +float64 precision boundary and the int64/uint64 limits, in both row orders and +with nested containers. These tests compare integers as Python ints: ordinary +NumPy int/float equality can itself round the expected integer and hide data +loss. Lossy mixtures must retain separately typed rows; exactly representable +mixtures must still load as arrays. These fast cases run in the existing wheel +smoke suite and need no additional CI job. + The runner also writes checkpoints using each implementation and reads them with the other. These cover ordinary parameters, RNG continuation, scalar and vector observations, and persisted results. Legacy `result.load()` is broken; diff --git a/test/pyalps/test_archive_dtypes.py b/test/pyalps/test_archive_dtypes.py index ff0b60529..b7ce78fde 100644 --- a/test/pyalps/test_archive_dtypes.py +++ b/test/pyalps/test_archive_dtypes.py @@ -61,6 +61,110 @@ def test_incompatible_python_numpy_rows_keep_groups(tmp_path, value): assert list(actual) == list(expected) +@pytest.mark.parametrize("reverse", [False, True]) +@pytest.mark.parametrize("nested", [False, True]) +@pytest.mark.parametrize( + "other_dtype,integer_dtype,integers", + [ + (np.int64, np.uint64, [2**63 + 1, 2**63 + 3]), + (np.uint64, np.int64, [2**53 + 1, 2**53 + 3]), + (np.float32, np.int64, [2**53 + 1, 2**53 + 3]), + (np.float64, np.int64, [-(2**53) - 1, -(2**53) - 3]), + (np.complex64, np.uint64, [2**64 - 1, 2**64 - 3]), + (np.complex128, np.int64, [2**63 - 1, -(2**63) + 1]), + (None, np.int64, [2**53 + 1, 2**53 + 3]), + ], +) +def test_lossy_mixed_rows_preserve_exact_integers( + tmp_path, other_dtype, integer_dtype, integers, reverse, nested +): + other = np.array([3, 4], dtype=other_dtype) if other_dtype else [3.0, 4.0] + # Exercise a noncontiguous, read-only integer row too. + row = np.repeat(np.array(integers, dtype=integer_dtype), 2)[::2] + row.flags.writeable = False + rows = [other, row] + if reverse: + rows.reverse() + value = [rows, rows] if nested else rows + filename = str(tmp_path / "exact-integers.h5") + with hdf5.archive(filename, "w") as archive: + archive["table"] = np.zeros((2, 2)) # Replace an existing dataset. + archive["table"] = value + with hdf5.archive(filename, "r") as archive: + assert archive.is_group("table") + restored = archive["table"] + for actual in restored if nested else [restored]: + integer_row = actual[0 if reverse else 1] + assert integer_row.dtype == integer_dtype + # NumPy mixed int/float equality would silently round the expected + # integers too. Compare independently converted Python ints. + assert [int(x) for x in integer_row] == integers + np.testing.assert_array_equal(actual[1 if reverse else 0], other) + + +@pytest.mark.parametrize("scalar", [int, np.int64]) +@pytest.mark.parametrize("nested", [False, True]) +def test_integer_scalar_rows_are_checked_before_stacking(tmp_path, scalar, nested): + integers = [2**53 + 1, 2**53 + 3] + rows = [np.array([1.5, 2.5]), tuple(scalar(x) for x in integers)] + value = [rows] if nested else rows + filename = str(tmp_path / "scalar-rows.h5") + with hdf5.archive(filename, "w") as archive: + archive["table"] = value + with hdf5.archive(filename, "r") as archive: + restored = archive["table"] + if nested: + restored = restored[0] + assert [int(x) for x in restored[1]] == integers + + +@pytest.mark.parametrize( + "other_dtype", [np.uint64, np.float32, np.float64, np.complex64] +) +@pytest.mark.parametrize("reverse", [False, True]) +@pytest.mark.parametrize("integers", [[0, 2**40], [2**53, 2**53 + 2], [-(2**63), 0]]) +def test_lossless_mixed_rows_still_stack(tmp_path, other_dtype, reverse, integers): + other = np.array([3, 4], dtype=other_dtype) + rows = [other, np.array(integers, dtype=np.int64)] + if reverse: + rows.reverse() + filename = str(tmp_path / "lossless.h5") + with hdf5.archive(filename, "w") as archive: + archive["table"] = {"old": 1} # Replace an existing group. + archive["table"] = rows + with hdf5.archive(filename, "r") as archive: + assert archive.is_data("table") + restored = archive["table"] + assert isinstance(restored, np.ndarray) + assert restored.shape == (2, 2) + assert restored.dtype == np.asarray(rows).dtype + assert [int(x.real) for x in restored[0 if reverse else 1]] == integers + + +def test_lossless_mixed_rows_allow_nan_and_infinity(tmp_path): + with hdf5.archive(str(tmp_path / "nonfinite.h5"), "w") as archive: + archive["table"] = [np.array([np.nan, np.inf]), np.array([2**53, 0])] + assert archive.is_data("table") + restored = archive["table"] + assert np.isnan(restored[0, 0]) and np.isposinf(restored[0, 1]) + assert [int(x) for x in restored[1]] == [2**53, 0] + + +@pytest.mark.parametrize("shape", [(), (0,), (2, 0)]) +def test_mixed_integer_array_shapes(tmp_path, shape): + rows = [np.zeros(shape, dtype=np.int64), np.full(shape, 2**64 - 1, dtype=np.uint64)] + filename = str(tmp_path / "shapes.h5") + with hdf5.archive(filename, "w") as archive: + archive["table"] = rows + with hdf5.archive(filename, "r") as archive: + restored = archive["table"] + if shape == (): + assert restored == [0, 2**64 - 1] + else: + assert isinstance(restored, np.ndarray) + assert restored.shape == (2, *shape) + + @pytest.mark.parametrize("dtype", [np.bool_, np.int8]) @pytest.mark.parametrize("shape", [(), (3,), (2, 3), (2, 1, 3), (0,), (2, 0)]) @pytest.mark.parametrize("attribute", [False, True]) From 9926259a0e26fe7de96067485a565d7b1bb79492 Mon Sep 17 00:00:00 2001 From: Wolf Date: Tue, 22 Sep 2026 09:32:53 -0700 Subject: [PATCH 51/52] Preserve native string parameter mutations --- bindings/python/pyalps/README.md | 16 +++++++++++++++ bindings/python/pyalps/cpp/ngs/params.cpp | 9 +++++---- bindings/python/pyalps/src/pyalps/mpi.py | 6 +++++- test/pyalps/native_params/check.py | 24 ++++++++++++++++++++++- test/pyalps/native_params/probe.cpp | 6 ++++++ test/pyalps/test_mpi_requests.py | 13 ++++++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index 15626aed0..bc008e687 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -76,6 +76,17 @@ metadata and out-of-range conversions raise an exception. Python metadata may use other shapes and containers supported by the HDF5 writer. Objects such as `None` can be held in memory but have no ALPS HDF5 representation. +Native C++ numeric and Boolean vectors become NumPy arrays when accessed from +Python. Native string vectors become lists so names can be replaced with longer +strings or appended without NumPy's fixed-width string truncation. These +materialized objects retain mutations for subsequent Python and C++ reads. +Explicitly supplied Python lists and NumPy arrays keep their original types. + +Integer conversion from text is range checked in the C++ SDK, including when +parameters originate outside Python. Negative text converted to an unsigned +integer now raises an exception instead of wrapping; replace negative textual +sentinels with an explicit value in the target type's range. + The C++ SDK remains independent of Python and nanobind. Python-owned values and their checkpoint decoder are supplied by the bindings. Rebuild downstream C++ extensions against the SDK from the same source revision as the wheel; the @@ -101,6 +112,11 @@ default object receive buffer. This adapter exchanges mpi4py messages; Boost.MPI's C++ serialization protocol and skeleton/content API are not wire compatible. Communicating processes must use the same protocol. +Communicator wrappers compare equal when their underlying mpi4py communicators +compare equal. They are intentionally unhashable, matching mpi4py. Unlike the +old Boost.MPI wrappers, they cannot be used as dictionary keys or set members; +applications needing such associations should use explicit application keys. + ## Versioning pyalps does not carry a version of its own. The numeric version is read from diff --git a/bindings/python/pyalps/cpp/ngs/params.cpp b/bindings/python/pyalps/cpp/ngs/params.cpp index 5253b3dae..55263c771 100644 --- a/bindings/python/pyalps/cpp/ngs/params.cpp +++ b/bindings/python/pyalps/cpp/ngs/params.cpp @@ -31,11 +31,12 @@ struct paramvalue_to_py_visitor : boost::static_visitor { } template nb::object operator()(std::vector const & value) const { - // An empty sequence has no elements from which NumPy can infer its - // type. Preserve the native family (especially Boolean masks). + // String parameters need variable-length elements: NumPy's inferred + // fixed-width Unicode dtype silently truncates longer replacements. if constexpr (std::is_same::value) - return alps::python::numpy_module().attr("array")( - nb::cast(value), nb::arg("dtype") = "str"); + return nb::cast(value); + // An empty numeric sequence has no elements from which NumPy can + // infer its type. Preserve the native family, including Boolean masks. else return alps::python::numpy_module().attr("array")( nb::cast(value), nb::arg("dtype") = alps::python::numpy_dtype::name); diff --git a/bindings/python/pyalps/src/pyalps/mpi.py b/bindings/python/pyalps/src/pyalps/mpi.py index 0ca59ce06..62c866401 100644 --- a/bindings/python/pyalps/src/pyalps/mpi.py +++ b/bindings/python/pyalps/src/pyalps/mpi.py @@ -170,7 +170,11 @@ class RequestList(list): class Communicator: - """Boost.MPI-compatible wrapper around an ``mpi4py.MPI.Comm``.""" + """Wrapper around an ``mpi4py.MPI.Comm`` with Boost.MPI operation names. + + Wrappers compare by their underlying communicator and are intentionally + unhashable, matching mpi4py rather than Boost.MPI's identity semantics. + """ def __init__(self, comm: Any = None): if isinstance(comm, Communicator): diff --git a/test/pyalps/native_params/check.py b/test/pyalps/native_params/check.py index 1e03a4363..b0c46d9e5 100644 --- a/test/pyalps/native_params/check.py +++ b/test/pyalps/native_params/check.py @@ -8,8 +8,30 @@ from pyalps import hdf5, ngs empty = native.empty_vectors() -for key, kind in {"integer": "i", "real": "f", "complex": "c", "boolean": "b", "text": "U"}.items(): +for key, kind in {"integer": "i", "real": "f", "complex": "c", "boolean": "b"}.items(): assert empty[key].size == 0 and empty[key].dtype.kind == kind +assert isinstance(empty["text"], list) and empty["text"] == [] + +# Native string vectors must allow names to grow. A NumPy array inferred +# from "Sz" has dtype U2 and silently changes "Magnetization" to "Ma". +parameters = native.string_parameters() +names = parameters["value"] +assert native.string_vector(parameters) == ["Sz"] +names[0] = "Magnetization" +assert names[0] == "Magnetization" +assert native.string_vector(parameters) == ["Magnetization"] +assert isinstance(names, list) +names.append("Susceptibility") +assert parameters["value"] is names +assert native.string_vector(parameters) == ["Magnetization", "Susceptibility"] + +# Explicit Python arrays keep their caller-selected representation. +names = np.array(["Sz"], dtype="U16") +parameters = ngs.params({"value": names}) +assert parameters["value"] is names +names[0] = "Magnetization" +assert parameters["value"].dtype == np.dtype("U16") +assert native.string_vector(parameters) == ["Magnetization"] values = np.array([1.0, 2.0]) parameters = ngs.params({"value": values}) diff --git a/test/pyalps/native_params/probe.cpp b/test/pyalps/native_params/probe.cpp index d6dab8840..f9ad670f0 100644 --- a/test/pyalps/native_params/probe.cpp +++ b/test/pyalps/native_params/probe.cpp @@ -14,6 +14,12 @@ NB_MODULE(parameter_probe, module) { module.def("complex_vector", [](alps::params const & p) { return p["value"].cast>>(); }); module.def("integer_vector", [](alps::params const & p) { return p["value"].cast>(); }); module.def("text", [](alps::params const & p) { return p["value"].cast(); }); + module.def("string_vector", [](alps::params const & p) { return p["value"].cast>(); }); + module.def("string_parameters", [] { + alps::params p; + p["value"] = std::vector{"Sz"}; + return p; + }); module.def("native_text", [] { alps::params p; p["value"] = std::vector{"", "middle", ""}; diff --git a/test/pyalps/test_mpi_requests.py b/test/pyalps/test_mpi_requests.py index d03b7419a..5068c5adc 100644 --- a/test/pyalps/test_mpi_requests.py +++ b/test/pyalps/test_mpi_requests.py @@ -9,6 +9,19 @@ from pyalps import mpi +def test_communicator_equality_and_hashability(): + from mpi4py import MPI + + wrappers = (mpi.world, mpi.Communicator(), mpi.Communicator(mpi.world)) + for comm in wrappers: + assert comm == mpi.world + assert not (comm != mpi.world) + assert comm != mpi.Communicator(MPI.COMM_NULL) + assert comm != object() + with pytest.raises(TypeError): + hash(comm) + + def poll(function): deadline = time.monotonic() + 15 while time.monotonic() < deadline: From 1a90f7d9e5aeb5a0f2638894127b18a0d76b045b Mon Sep 17 00:00:00 2001 From: Marcus Rosales Date: Tue, 22 Sep 2026 14:36:15 -0400 Subject: [PATCH 52/52] Pin nanobind and reject mismatched downstream ABIs The wheel build allowed any nanobind>=2.10,<3, a range that spans internals ABIs v17-v21. A downstream module built with a different ABI than the wheel cannot see pyalps types, and nanobind aborts the interpreter on import: Critical nanobind error: nb_type_new("sim"): base type "alps::mcbase" not known to nanobind! Pin nanobind==2.15.0 for the build and cibuildwheel tests, record the nanobind version and internals ABI in pyalps_config.py, and have alps_target_link_pyalps fail at configure time with install instructions when a consumer's nanobind ABI differs. Co-Authored-By: Claude Opus 5.5 --- bindings/python/pyalps/CMakeLists.txt | 8 ++++ bindings/python/pyalps/README.md | 10 +++++ bindings/python/pyalps/pyproject.toml | 12 +++--- .../pyalps/src/pyalps/pyalps_config.py.in | 5 +++ cmake/UsePyALPS.cmake | 39 +++++++++++++++++++ tutorials/ngs/5_export_python/README.md | 5 ++- 6 files changed, 73 insertions(+), 6 deletions(-) diff --git a/bindings/python/pyalps/CMakeLists.txt b/bindings/python/pyalps/CMakeLists.txt index a59ccd632..377518ee7 100644 --- a/bindings/python/pyalps/CMakeLists.txt +++ b/bindings/python/pyalps/CMakeLists.txt @@ -208,6 +208,14 @@ if(PYALPS_BUNDLE_APPLICATIONS) else() set(PYALPS_ALPS_BIN_FALLBACK "${ALPS_ROOT_DIR}/bin") endif() +# Record the nanobind ABI so alps_target_link_pyalps can reject downstream +# modules that would not share pyalps types. +include("${ALPS_PYTHON_USE_FILE}") +set(PYALPS_NANOBIND_INTERNALS_VERSION "") +if(COMMAND alps_nanobind_internals_version) # absent from older SDKs + alps_nanobind_internals_version(PYALPS_NANOBIND_INTERNALS_VERSION) +endif() +set(PYALPS_NANOBIND_VERSION "${nanobind_VERSION}") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/pyalps/pyalps_config.py.in" "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" @ONLY) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/pyalps_config.py" DESTINATION pyalps) diff --git a/bindings/python/pyalps/README.md b/bindings/python/pyalps/README.md index bc008e687..a7853e1ac 100644 --- a/bindings/python/pyalps/README.md +++ b/bindings/python/pyalps/README.md @@ -92,6 +92,16 @@ their checkpoint decoder are supplied by the bindings. Rebuild downstream C++ extensions against the SDK from the same source revision as the wheel; the parameter layout changed during this migration. +Downstream nanobind modules must also use the same nanobind internals ABI as +the installed wheel; otherwise nanobind cannot see pyalps types and aborts the +interpreter at import (`base type "alps::mcbase" not known to nanobind`). The +wheel is built with the nanobind version pinned in `pyproject.toml` and records +its ABI in `pyalps.pyalps_config`; `alps_target_link_pyalps` rejects a +mismatched nanobind at CMake configure time. Install the matching release, for +example `python -m pip install "nanobind==$(python -c 'import +pyalps.pyalps_config as c; print(c.NANOBIND_VERSION)')"`. The compiler's C++ +standard library must also match (libc++ on macOS, libstdc++ on Linux). + New HDF5 writes distinguish Boolean and signed-byte values with an `__alps_type__` attribute while retaining the existing numeric storage format. Unmarked signed-byte data from old ALPS files retains the legacy Boolean diff --git a/bindings/python/pyalps/pyproject.toml b/bindings/python/pyalps/pyproject.toml index d89f34476..1a8899f61 100644 --- a/bindings/python/pyalps/pyproject.toml +++ b/bindings/python/pyalps/pyproject.toml @@ -1,8 +1,10 @@ [build-system] -# nanobind is capped below the next major: all extension modules in one -# process must agree on the nanobind ABI, and its API/ABI may break at -# major versions. Bump the cap deliberately, with a full test run. -requires = ["scikit-build-core>=1.0", "nanobind>=2.10,<3"] +# nanobind is pinned exactly: downstream modules that derive from pyalps +# types must share its internals ABI, which changes within 2.x minor +# releases (2.10 -> v17, 2.15 -> v21). An unpinned build would let every +# wheel pick up whatever is newest. Bump deliberately, with a full test run, +# and keep the test-requires pin below in step. +requires = ["scikit-build-core>=1.0", "nanobind==2.15.0"] build-backend = "scikit_build_core.build" [project] @@ -125,7 +127,7 @@ repair-wheel-command = [ select = "cp314-*" inherit.environment = "append" environment = { PYALPS_TEST_DOWNSTREAM_EXPORT = "1" } -test-requires = ["pytest", "nanobind>=2.10,<3"] +test-requires = ["pytest", "nanobind==2.15.0"] [[tool.cibuildwheel.overrides]] select = "*-macosx_*" diff --git a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in index c7e88c7c6..14d7f0bcb 100644 --- a/bindings/python/pyalps/src/pyalps/pyalps_config.py.in +++ b/bindings/python/pyalps/src/pyalps/pyalps_config.py.in @@ -11,3 +11,8 @@ # that make two builds of identical source differ. ALPS_XML_INSTALL_DIR="@PYALPS_ALPS_XML_FALLBACK@" ALPS_BIN_INSTALL_DIR="@PYALPS_ALPS_BIN_FALLBACK@" + +# nanobind that built these extensions. Downstream modules deriving from pyalps +# types must use the same internals ABI; cmake/UsePyALPS.cmake checks it. +NANOBIND_VERSION="@PYALPS_NANOBIND_VERSION@" +NANOBIND_INTERNALS_VERSION="@PYALPS_NANOBIND_INTERNALS_VERSION@" diff --git a/cmake/UsePyALPS.cmake b/cmake/UsePyALPS.cmake index caa5f9ea6..e072f00c9 100644 --- a/cmake/UsePyALPS.cmake +++ b/cmake/UsePyALPS.cmake @@ -15,6 +15,19 @@ include_guard(GLOBAL) +# nanobind shares types between extension modules only within one internals +# ABI (NB_INTERNALS_VERSION), which changes within 2.x minor releases. Read it +# from the nanobind found by find_package(nanobind); empty if unavailable. +function(alps_nanobind_internals_version out_var) + set(_version "") + if(NB_DIR AND EXISTS "${NB_DIR}/src/nb_abi.h") + file(STRINGS "${NB_DIR}/src/nb_abi.h" _line + REGEX "^#[ \t]*define[ \t]+NB_INTERNALS_VERSION[ \t]+[0-9]+") + string(REGEX MATCH "[0-9]+$" _version "${_line}") + endif() + set(${out_var} "${_version}" PARENT_SCOPE) +endfunction() + function(alps_target_link_pyalps target) if(NOT TARGET "${target}") message(FATAL_ERROR @@ -53,6 +66,32 @@ function(alps_target_link_pyalps target) ERROR_QUIET) if(_pyalps_location_result EQUAL 0 AND _pyalps_package_dir) + # A module built on a different nanobind internals ABI cannot see pyalps + # types, and nanobind aborts the interpreter when it is imported. Fail + # here instead. Older packages that do not record their ABI are skipped. + alps_nanobind_internals_version(_consumer_nb_abi) + set(_pyalps_config "${_pyalps_package_dir}/pyalps_config.py") + if(_consumer_nb_abi AND EXISTS "${_pyalps_config}") + file(STRINGS "${_pyalps_config}" _pyalps_nb_lines + REGEX "^NANOBIND_(VERSION|INTERNALS_VERSION)=") + string(REGEX MATCH "NANOBIND_INTERNALS_VERSION=\"([0-9]+)\"" + _match "${_pyalps_nb_lines}") + set(_pyalps_nb_abi "${CMAKE_MATCH_1}") + string(REGEX MATCH "NANOBIND_VERSION=\"([^\"]*)\"" + _match "${_pyalps_nb_lines}") + set(_pyalps_nb_version "${CMAKE_MATCH_1}") + if(_pyalps_nb_abi AND NOT _pyalps_nb_abi STREQUAL _consumer_nb_abi) + message(FATAL_ERROR + "${target}: nanobind ${nanobind_VERSION} (internals ABI " + "v${_consumer_nb_abi}) does not match the nanobind ${_pyalps_nb_version} " + "(ABI v${_pyalps_nb_abi}) that built pyalps at ${_pyalps_package_dir}. " + "The module could not use pyalps types and would abort Python on " + "import. Install the matching release, e.g. " + "'${PYALPS_PYTHON_EXECUTABLE} -m pip install nanobind==${_pyalps_nb_version}', " + "and reconfigure with a fresh build directory.") + endif() + endif() + # A repair-tool directory is what marks this install as a relocated wheel: # auditwheel writes /pyalps.libs, delocate writes # pyalps/.dylibs. diff --git a/tutorials/ngs/5_export_python/README.md b/tutorials/ngs/5_export_python/README.md index 67fed0ce4..fcffa71ef 100644 --- a/tutorials/ngs/5_export_python/README.md +++ b/tutorials/ngs/5_export_python/README.md @@ -2,9 +2,12 @@ This example replaces the former Boost.Python export tutorial while retaining the public `ALPS_EXPORT_SIM_TO_PYTHON` helper. Build it against an installed -ALPS SDK and the Python environment containing pyalps and nanobind: +ALPS SDK and the Python environment containing pyalps and nanobind. The +nanobind release must match the one that built pyalps; `alps_target_link_pyalps` +stops at configure time if it does not: ```sh +python -m pip install "nanobind==$(python -c 'import pyalps.pyalps_config as c; print(c.NANOBIND_VERSION)')" cmake -S . -B build -GNinja \ -DALPS_DIR=/path/to/alps/share/alps \ -DPython_EXECUTABLE="$(command -v python)"