From 96c91e9a46205aafacc5a0b97eae3ef048f09623 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 16:12:03 +0200 Subject: [PATCH 1/8] Replace Travis with ALPS-style GitHub Actions --- .github/workflows/build.yml | 198 ++++++++++++++++++++++++++++++++++++ .travis.yml | 75 -------------- ALPS3_MIGRATION_PLAN.md | 6 +- README.md | 14 ++- 4 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d7bd50e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,198 @@ +name: Maxent CI + +on: + push: + branches: + - master + - modernize/step2 + pull_request: + branches: + - master + - modernize/step2 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + BOOST_VERSION: "1.91.0" + BOOST_VERSION_U: "1_91_0" + +jobs: + linux-build: + name: >- + Maxent / ${{ matrix.plat.os }} / ${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }} / + ${{ matrix.plat.variant }} + runs-on: ${{ matrix.plat.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + plat: + # Match the compiler installation conventions used by ALPS 3.0 CI. + - { os: ubuntu-22.04, comp_pack: "gcc-11 g++-11", c_compiler: gcc, cxx_compiler: g++, c_version: 11, variant: strict } + - { os: ubuntu-24.04, comp_pack: "gcc-13 g++-13", c_compiler: gcc, cxx_compiler: g++, c_version: 13, variant: strict } + - { os: ubuntu-24.04, comp_pack: "gcc-15 g++-15", c_compiler: gcc, cxx_compiler: g++, c_version: 15, variant: strict } + - { os: ubuntu-22.04, comp_pack: "clang-15", c_compiler: clang, cxx_compiler: clang++, c_version: 15, variant: strict } + - { os: ubuntu-24.04, comp_pack: "clang-19", c_compiler: clang, cxx_compiler: clang++, c_version: 19, variant: strict } + - { os: ubuntu-24.04, comp_pack: "gcc-13 g++-13", c_compiler: gcc, cxx_compiler: g++, c_version: 13, variant: lapack, lapack: true } + - { os: ubuntu-24.04, comp_pack: "clang-19", c_compiler: clang, cxx_compiler: clang++, c_version: 19, variant: asan-ubsan, sanitizers: true } + - { os: ubuntu-24.04, comp_pack: "gcc-13 g++-13", c_compiler: gcc, cxx_compiler: g++, c_version: 13, variant: alps-flags, alps_flags: true } + + steps: + - uses: actions/checkout@v7 + - name: Check out ALPSCore + uses: actions/checkout@v7 + with: + repository: ALPSCore/ALPSCore + path: alpscore + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + - name: Install dependencies + timeout-minutes: 20 + run: | + printf 'Acquire::Retries "3";\nAcquire::http::Timeout "30";\nAcquire::https::Timeout "30";\n' \ + | sudo tee /etc/apt/apt.conf.d/99ci-timeouts > /dev/null + sudo apt-get update + sudo apt-get install -y liblapack-dev libblas-dev libopenmpi-dev libhdf5-serial-dev libeigen3-dev + if [[ "${{ matrix.plat.c_compiler }}" == "clang" && "${{ matrix.plat.c_version }}" -ge 19 ]]; then + wget -nv --tries=3 --timeout=60 -O /tmp/llvm.sh https://apt.llvm.org/llvm.sh + sudo bash /tmp/llvm.sh "${{ matrix.plat.c_version }}" all + else + if [[ "${{ matrix.plat.c_compiler }}" == "gcc" && "${{ matrix.plat.c_version }}" -ge 15 ]]; then + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + fi + sudo apt-get install -y ${{ matrix.plat.comp_pack }} + fi + python -m pip install --upgrade pip + python -m pip install "numpy>=1.26" "h5py>=3.10" + - name: Cache Boost and ALPSCore + id: cache-dependencies + uses: actions/cache@v4 + with: + path: | + ${{ runner.temp }}/boost + ${{ runner.temp }}/alpscore-install + key: >- + maxent-deps-${{ runner.os }}-${{ runner.arch }}-${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }}- + boost-${{ env.BOOST_VERSION }}-${{ hashFiles('alpscore/**') }} + - name: Build Boost and ALPSCore + if: steps.cache-dependencies.outputs.cache-hit != 'true' + env: + CC: ${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }} + CXX: ${{ matrix.plat.cxx_compiler }}-${{ matrix.plat.c_version }} + run: | + wget -nv --tries=3 --timeout=60 --waitretry=10 \ + "https://archives.boost.io/release/${BOOST_VERSION}/source/boost_${BOOST_VERSION_U}.tar.gz" + tar -xzf "boost_${BOOST_VERSION_U}.tar.gz" + cd "boost_${BOOST_VERSION_U}" + ./bootstrap.sh --prefix="${RUNNER_TEMP}/boost" --with-libraries=program_options + ./b2 -j 2 install + cd .. + cmake -S alpscore -B alpscore-build \ + -DTesting=OFF \ + -DALPS_CXX_STD=c++17 \ + -DBoost_ROOT="${RUNNER_TEMP}/boost" \ + -DCMAKE_PREFIX_PATH="${RUNNER_TEMP}/boost" \ + -DCMAKE_INSTALL_PREFIX="${RUNNER_TEMP}/alpscore-install" + cmake --build alpscore-build -j 2 + cmake --install alpscore-build + - name: Configure Maxent + env: + CC: ${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }} + CXX: ${{ matrix.plat.cxx_compiler }}-${{ matrix.plat.c_version }} + run: | + extra_flags="" + if [[ "${{ matrix.plat.sanitizers || false }}" == "true" ]]; then + extra_flags="-fsanitize=address,undefined -fno-omit-frame-pointer" + elif [[ "${{ matrix.plat.alps_flags || false }}" == "true" ]]; then + extra_flags="-fpermissive -DBOOST_NO_AUTO_PTR -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF -DBOOST_TIMER_ENABLE_DEPRECATED" + fi + cmake -S . -B build/ci \ + -DCMAKE_BUILD_TYPE=${{ matrix.plat.sanitizers && 'Debug' || 'Release' }} \ + -DCMAKE_PREFIX_PATH="${RUNNER_TEMP}/alpscore-install;${RUNNER_TEMP}/boost" \ + -DALPSCore_DIR="${RUNNER_TEMP}/alpscore-install/share/ALPSCore" \ + -DBoost_ROOT="${RUNNER_TEMP}/boost" \ + -DMAXENT_USE_LAPACK=${{ matrix.plat.lapack && 'ON' || 'OFF' }} \ + -DMAXENT_WERROR=ON \ + -DCMAKE_CXX_FLAGS="${extra_flags}" \ + -DCMAKE_EXE_LINKER_FLAGS="${extra_flags}" + - name: Build Maxent + run: cmake --build build/ci -j 2 + - name: Test Maxent + env: + ASAN_OPTIONS: detect_leaks=1 + UBSAN_OPTIONS: print_stacktrace=1 + run: ctest --test-dir build/ci --output-on-failure + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: test-logs-${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }}-${{ matrix.plat.variant }} + path: build/ci/Testing/Temporary/** + + macos-build: + name: Maxent / macos-15 / AppleClang / strict + runs-on: macos-15 + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + - name: Check out ALPSCore + uses: actions/checkout@v7 + with: + repository: ALPSCore/ALPSCore + path: alpscore + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + - name: Install dependencies + timeout-minutes: 30 + run: | + brew install boost eigen hdf5 open-mpi + python -m pip install --upgrade pip + python -m pip install "numpy>=1.26" "h5py>=3.10" + - name: Cache ALPSCore + id: cache-alpscore + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/alpscore-install + key: >- + maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-appleclang- + ${{ hashFiles('alpscore/**') }} + - name: Build ALPSCore + if: steps.cache-alpscore.outputs.cache-hit != 'true' + env: + CC: /usr/bin/clang + CXX: /usr/bin/clang++ + run: | + cmake -S alpscore -B alpscore-build \ + -DTesting=OFF \ + -DALPS_CXX_STD=c++17 \ + -DCMAKE_PREFIX_PATH="$(brew --prefix)" \ + -DCMAKE_INSTALL_PREFIX="${RUNNER_TEMP}/alpscore-install" + cmake --build alpscore-build -j "$(sysctl -n hw.ncpu)" + cmake --install alpscore-build + - name: Configure Maxent + env: + CC: /usr/bin/clang + CXX: /usr/bin/clang++ + run: | + cmake -S . -B build/ci \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="${RUNNER_TEMP}/alpscore-install;$(brew --prefix)" \ + -DALPSCore_DIR="${RUNNER_TEMP}/alpscore-install/share/ALPSCore" \ + -DMAXENT_WERROR=ON + - name: Build Maxent + run: cmake --build build/ci -j "$(sysctl -n hw.ncpu)" + - name: Test Maxent + run: ctest --test-dir build/ci --output-on-failure + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: test-logs-macos-15-appleclang + path: build/ci/Testing/Temporary/** diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bbd29c6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,75 +0,0 @@ -#based on ALPSCore/CT-HYB .travis.yml -language: cpp -sudo: required -dist: trusty - -compiler: - - gcc - - clang - -branches: - only: - - master - - GPLv3 - #- travis # To debug .travis.yml - -addons: - apt: - sources: - # Boost 1.58 - - sourceline: ppa:kzemek/boost - packages: - - libboost1.58-dev - - libboost-program-options1.58-dev - - libboost-program-options1.58.0 - - openmpi-bin - - openmpi-common - - openmpi-doc - - libopenmpi-dev - - libhdf5-serial-dev - - gsl-bin - - libgsl0-dev - -install: true - -before_script: - - export OMPI_CC=${CC} - - export OMPI_CXX=${CXX} - - export LD_LIBRARY_PATH=/usr/local/lib/:${LD_LIBRARY_PATH} - -script: - # Stop on first error - - set -e - - # Create directory for installed prerequisites - - export PREREQS_DIR=$(readlink -f $TRAVIS_BUILD_DIR/../installed) - - mkdir $PREREQS_DIR - - # Install ALPSCore wo tests - - cd $TRAVIS_BUILD_DIR/.. - - git clone https://github.com/ALPSCore/ALPSCore.git ALPSCore.git - - mkdir ALPSCore.build && pushd ALPSCore.build - - | - cmake ../ALPSCore.git \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpic++ \ - -DCMAKE_INSTALL_PREFIX=$PREREQS_DIR/ALPSCore \ - -DALPS_INSTALL_EIGEN=true \ - -DTesting=false \ - -DENABLE_MPI=ON - - make -j3 - - make install - - export ALPSCore_DIR=$PREREQS_DIR/ALPSCore - - # Build and test Maxent - - cd $TRAVIS_BUILD_DIR/.. - - mkdir build - - cd build - - | - cmake ../Maxent \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpic++ \ - -DCMAKE_INSTALL_PREFIX=$TRAVIS_BUILD_DIR/installed \ - - make -j3 - - make test diff --git a/ALPS3_MIGRATION_PLAN.md b/ALPS3_MIGRATION_PLAN.md index d36dd4d..29b1f2a 100644 --- a/ALPS3_MIGRATION_PLAN.md +++ b/ALPS3_MIGRATION_PLAN.md @@ -436,7 +436,8 @@ Keep the `MaxEntSimulation` public getters stable, because the tests use them. ### 2.4 Hygiene and CI -* Delete `.travis.yml` and add GitHub Actions with this matrix: +* **In progress 2026-09-24:** delete `.travis.yml` and add GitHub Actions, + mirroring ALPS 3.0's workflow conventions, with this matrix: * Ubuntu: GCC 11/13/15, Clang 15/19 * macOS arm64: AppleClang * one ASan/UBSan job @@ -445,7 +446,8 @@ Keep the `MaxEntSimulation` public getters stable, because the tests use them. Build ALPSCore from source in CI and cache it. * **Replace the allowlist `.gitignore`.** Done 2026-09-23: replaced with a minimal ignore list (build dirs, OS/editor files, Python caches). Local-only material (D10) is hidden per clone via `.git/info/exclude`, not in the repository. * ~~Remove the dead Pade sources~~ Done: Pade was removed entirely (D7). -* Update the README: build instructions, dependency list (no GSL), and remove the Travis badge. +* **Done 2026-09-24:** update the README build/test instructions and dependency + list, and replace the Travis badge with the GitHub Actions badge. ### 2.5 Isolate ALPSCore behind two seams (bridge into step 4) diff --git a/README.md b/README.md index 2071d44..9c407dd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Maxent ====== -[![Build Status](https://travis-ci.org/CQMP/Maxent.svg?branch=master)](https://travis-ci.org/CQMP/Maxent) +[![CI](https://github.com/CQMP/Maxent/actions/workflows/build.yml/badge.svg)](https://github.com/CQMP/Maxent/actions/workflows/build.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) The Maxent Project: A utility for performing analytic continuation using the method of Maximum Entropy. @@ -38,10 +38,15 @@ Table of Contents ### Libraries #### Boost -When compiling both ALPSCore and Maxent, be careful to ensure boost was compiled with the same library and stdlib as ALPSCore and Maxent. +Maxent uses Boost headers and the `program_options` library in its utilities. +Use the same Boost version and C++ standard library that were used to build +ALPSCore; CMake checks the version reported by ALPSCore. #### ALPSCore -ALPSCore needs to be properly installed, see [ALPSCore library](https://github.com/ALPSCore/ALPSCore). ALPSCore provides the location of the Boost libraries. +ALPSCore must be installed, including its CMake package files; see the +[ALPSCore library](https://github.com/ALPSCore/ALPSCore). Set `ALPSCore_DIR` +to the directory containing `ALPSCoreConfig.cmake` when it is not found +automatically. #### Eigen3 For our linear algebra routines we use Eigen3 version >=3.3. CMake finds it through its `Eigen3Config.cmake`; if it is not in a standard location, add its prefix to `CMAKE_PREFIX_PATH` or set `-DEigen3_DIR=/path/to/share/eigen3/cmake`. @@ -80,7 +85,8 @@ Options: | `MAXENT_USE_SYSTEM_GTEST` | OFF | use an installed GoogleTest instead of downloading 1.18.0 | ### Tests -Once compiled, run `ctest --test-dir build` (or `ctest --preset ` for +Once compiled, run `ctest --test-dir build --output-on-failure` (or +`ctest --preset ` for a preset build) to make sure everything works. This runs the unit tests (label `unit`) and the regression suite (label `regression-fast`, needs Python 3 with numpy and h5py); see [test/regression/README.md](test/regression/README.md). From affa5c1c9b582d7221dacdc760b175eff4addf05 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 16:15:10 +0200 Subject: [PATCH 2/8] Point macOS ALPSCore build at Homebrew Eigen --- .github/workflows/build.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7bd50e..9b2f76a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,9 +76,7 @@ jobs: path: | ${{ runner.temp }}/boost ${{ runner.temp }}/alpscore-install - key: >- - maxent-deps-${{ runner.os }}-${{ runner.arch }}-${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }}- - boost-${{ env.BOOST_VERSION }}-${{ hashFiles('alpscore/**') }} + key: maxent-deps-${{ runner.os }}-${{ runner.arch }}-${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }}-boost-${{ env.BOOST_VERSION }}-${{ hashFiles('alpscore/**') }} - name: Build Boost and ALPSCore if: steps.cache-dependencies.outputs.cache-hit != 'true' env: @@ -160,9 +158,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ runner.temp }}/alpscore-install - key: >- - maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-appleclang- - ${{ hashFiles('alpscore/**') }} + key: maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-appleclang-${{ hashFiles('alpscore/**') }} - name: Build ALPSCore if: steps.cache-alpscore.outputs.cache-hit != 'true' env: @@ -173,6 +169,7 @@ jobs: -DTesting=OFF \ -DALPS_CXX_STD=c++17 \ -DCMAKE_PREFIX_PATH="$(brew --prefix)" \ + -DEIGEN3_INCLUDE_DIR="$(brew --prefix eigen)/include/eigen3" \ -DCMAKE_INSTALL_PREFIX="${RUNNER_TEMP}/alpscore-install" cmake --build alpscore-build -j "$(sysctl -n hw.ncpu)" cmake --install alpscore-build From e92b1469f8b8af8a947e6d29ff4e25c3369913c6 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 16:19:45 +0200 Subject: [PATCH 3/8] Support current Eigen and GitHub runners --- .github/workflows/build.yml | 4 ++-- CMakeLists.txt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b2f76a..9a3f076 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,7 @@ jobs: python -m pip install "numpy>=1.26" "h5py>=3.10" - name: Cache Boost and ALPSCore id: cache-dependencies - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: | ${{ runner.temp }}/boost @@ -155,7 +155,7 @@ jobs: python -m pip install "numpy>=1.26" "h5py>=3.10" - name: Cache ALPSCore id: cache-alpscore - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ${{ runner.temp }}/alpscore-install key: maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-appleclang-${{ hashFiles('alpscore/**') }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 38ba864..a474254 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,10 @@ if(NOT Boost_FOUND) message(FATAL_ERROR "Boost ${ALPSCore_BOOST_VERSION} (the version ALPSCore was built with) was not " "found. Pass -DBoost_DIR= or add its prefix to CMAKE_PREFIX_PATH.") endif() -find_package(Eigen3 3.3 CONFIG REQUIRED) +find_package(Eigen3 CONFIG REQUIRED) +if(Eigen3_VERSION VERSION_LESS 3.3) + message(FATAL_ERROR "Eigen 3.3 or newer is required (found ${Eigen3_VERSION})") +endif() set(HAVE_BLAS 0) set(HAVE_LAPACK 0) From 40ed20c6ad97a00b0848e9defcf1324615d04e96 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 16:29:56 +0200 Subject: [PATCH 4/8] Include iomanip for std::setw --- src/maxent.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/maxent.cpp b/src/maxent.cpp index 5fdba78..d5aa0c3 100644 --- a/src/maxent.cpp +++ b/src/maxent.cpp @@ -12,6 +12,7 @@ #include "maxent.hpp" #include +#include int main(int argc,const char** argv) { From bd8670e625d4e8406df64bd0797a654084470e67 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Fri, 25 Sep 2026 09:13:06 +0200 Subject: [PATCH 5/8] Make regression results portable across platforms --- .github/lsan.supp | 3 +++ .github/workflows/build.yml | 1 + CMakeLists.txt | 6 +++++- src/default_model.cpp | 8 +++++--- src/maxent_helper.cpp | 10 ++++++++-- test/regression/PROVENANCE.md | 7 +++++++ test/regression/REFERENCE_CHANGES.md | 15 +++++++++++++++ test/regression/reference/components.h5 | Bin 74640 -> 68496 bytes test/regression/reference/t_generate_err.h5 | Bin 33656 -> 27512 bytes .../reference/t_generate_err_seed.h5 | Bin 33656 -> 27512 bytes 10 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 .github/lsan.supp diff --git a/.github/lsan.supp b/.github/lsan.supp new file mode 100644 index 0000000..086b036 --- /dev/null +++ b/.github/lsan.supp @@ -0,0 +1,3 @@ +# HDF5 1.10 on Ubuntu 24.04 retains attribute-read allocations at exit. +# Keep LeakSanitizer enabled while excluding this external-library leak. +leak:H5A__read diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a3f076..4022898 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,6 +123,7 @@ jobs: - name: Test Maxent env: ASAN_OPTIONS: detect_leaks=1 + LSAN_OPTIONS: suppressions=${{ github.workspace }}/.github/lsan.supp UBSAN_OPTIONS: print_stacktrace=1 run: ctest --test-dir build/ci --output-on-failure - name: Upload test logs on failure diff --git a/CMakeLists.txt b/CMakeLists.txt index a474254..81acdb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,11 @@ configure_file(cmake/maxent_config.hpp.in ${PROJECT_BINARY_DIR}/config/maxent_co add_library(maxent_warnings INTERFACE) target_compile_options(maxent_warnings INTERFACE $<$:-Wall -Wextra -Wpedantic> - $<$,$>:-Werror>) + $<$,$>:-Werror> + # GCC diagnoses Boost.Variant's type-erased storage as maybe-uninitialized + # when ALPS parameters are instantiated. Keep the warning visible without + # treating this third-party false positive as a Maxent build failure. + $<$,$>:-Wno-error=maybe-uninitialized>) # ---------------------------------------------------------------- library and program add_library(maxent_core STATIC diff --git a/src/default_model.cpp b/src/default_model.cpp index 5ad85c9..3ca82a1 100644 --- a/src/default_model.cpp +++ b/src/default_model.cpp @@ -113,8 +113,11 @@ double GeneralDefaultModel::norm() { //this is an evaluation on an equidistant grid; sum integrated by trapezoidal rule double delta_omega = (omega_max - omega_min) / (ntab - 1); for (int o = 1; o < ntab; ++o) { - double omega1 = omega_min + (o - 1) * delta_omega; - double omega2 = omega_min + o * delta_omega; + // This weighted-endpoint form gives an exact zero on symmetric grids. + // That matters for models with a deliberately discontinuous zero branch + // and avoids compiler-dependent FMA rounding at the branch point. + double omega1 = ((ntab - o) * omega_min + (o - 1) * omega_max) / (ntab - 1); + double omega2 = ((ntab - 1 - o) * omega_min + o * omega_max) / (ntab - 1); sum += ((*Mod)(omega1) + (*Mod)(omega2)) / 2. * delta_omega; xtab[o] = sum; } @@ -191,4 +194,3 @@ std::shared_ptr make_default_model(const alps::params& parms, std: } - diff --git a/src/maxent_helper.cpp b/src/maxent_helper.cpp index 406a8ad..672b2ad 100644 --- a/src/maxent_helper.cpp +++ b/src/maxent_helper.cpp @@ -384,8 +384,14 @@ vector_type MaxEntHelper::generateGaussNoise(vector_type data, vector_type err,s const int N = data.size(); vector_type data_noise(N); for(int i=0;i s(data[i],err[i]); - data_noise[i] = s(rng); + // std::normal_distribution does not prescribe its transformation and + // therefore gives different seeded sequences with libc++ and + // libstdc++. Convert mt19937 output explicitly so bootstrap results + // are reproducible across supported platforms. + const double u1 = std::ldexp(static_cast(rng()) + 0.5, -32); + const double u2 = std::ldexp(static_cast(rng()) + 0.5, -32); + const double z = std::sqrt(-2.0 * std::log(u1)) * std::cos(2.0 * M_PI * u2); + data_noise[i] = data[i] + err[i] * z; } return data_noise; } diff --git a/test/regression/PROVENANCE.md b/test/regression/PROVENANCE.md index 8f3e1b7..7c52fd1 100644 --- a/test/regression/PROVENANCE.md +++ b/test/regression/PROVENANCE.md @@ -65,6 +65,13 @@ generations were identical. `t_generate_err_seed.h5` was added after commit default-seed reference, so the case detects an ignored `SEED` parameter. Both references include `case.out.booterr.dat`. +On 2026-09-25 the two bootstrap references were regenerated on the same +AppleClang/libc++ system after replacing `std::normal_distribution` with an +explicit transform of `mt19937` output. This makes the seeded sequence +independent of the standard-library implementation. The component reference +was regenerated at the same time after making the symmetric normalization +grid's zero crossing exact; only `model_general_double_gaussian.txt` changed. + ## Time-bosonic references updated (B7b, 2026-09-24) The `kernel_time_bosonic` datasets in `components.h5` and the diff --git a/test/regression/REFERENCE_CHANGES.md b/test/regression/REFERENCE_CHANGES.md index ebb3feb..de08339 100644 --- a/test/regression/REFERENCE_CHANGES.md +++ b/test/regression/REFERENCE_CHANGES.md @@ -4,6 +4,21 @@ Every regeneration of files in `reference/` is logged here: date, commit, reason, affected cases, and the largest difference to the previous references (from `compare.py --report`). Newest entry first. +## 2026-09-25: cross-library deterministic bootstrap and zero crossing + +* **Source:** `modernize/step2.4`, before the CI portability-fix commit. +* **Reason:** `std::normal_distribution` does not specify its transformation, + so identical `mt19937` seeds produced different bootstrap samples with + libc++ and libstdc++. Maxent now performs an explicit Box-Muller transform. + The default-model integration grid now constructs a symmetric midpoint as + exact zero, avoiding compiler-dependent FMA rounding at the discontinuous + branch of the general double Gaussian. +* **Cases:** `t_generate_err`, `t_generate_err_seed`, and `components`. + Only the two `booterr.dat` datasets and + `model_general_double_gaussian.txt` changed. +* **Largest differences:** 3.742e-2 relative in bootstrap output and 4.077e-4 + relative in the general-double-Gaussian component. + ## 2026-09-24: explicit bootstrap seed coverage * **Source:** `modernize/step2.3c` after commit `8b2ff05`. diff --git a/test/regression/reference/components.h5 b/test/regression/reference/components.h5 index 088f09ac2c963582a86f498160e7942b50af60e6..3b108156bfe978b8272829f9cd4e1afbbf6b73fd 100644 GIT binary patch delta 2151 zcmW;MeQZ|M9S89Ko|dOQEo~PQ7oSJct5G9&lES266}CR0ym*0C=OzoHJd2xbmu?+% zbaRV#1c-7lG}W*5h?{LdQn>?K&EsUSx-#s`4s>M%s-t$TbBpta+8697FNN-V{pa)j zo|BXN+I)>9S(rU0Gj7>?`mwRh+!={xnE~&B zBX7c|_dwGG0{RRry#*m%0d>%kQ*db#9^DV0u7aj#5l{z~)*__Ij5?GOxdUE{BU|9p zJD}-l1avtpZAM6wP_2&iz@_(Qyw)KlH^M#Qh!2l`9X{>O=p#cC-Q=GfDT1cw5YT0? zbS6Uj22`6PC2;BLjMtWm%Lw%GlpKLQ?npL5+6wiaBa7kE74YZ>@aetK^nC>MZdf{u zkj{eYaO8Hl^euSwtMKVsX!NYM_tQD;x5io~P~4f2PPmlkwTfAGB_M1f{Hdj?O;x ztR*Cul>EV{k+#pBHwwTawgqxtJM|FRjyClc|hEl%T452Ex$GI!nOn?6V+ zI`9Ce|K7WH?%O{`^@(J>qUz;e_f(;dwJ&w?(9`qK!o{1K%kNyDcMhkx_}GOTwvSF? zgex{&zWBtvttdK~j7x1+{qSy-vhI5*qtA7rlC^KgzWbjoL^W$u-+fOU{W8$_ncpoaFMke2J<0fjSxw_hHlmbu^XBjE z-S#>vS#@5;vJZV!v(B~4)!t>OV{MtUpzrScGA%tv3uDthHujfD4s?I?l?Aj4mfnD{ zKP_eOK6GRpK79_F4j`agVQDTx+6Fb?$Qrn`1m1uXJ2!}p0~LJn0!}r+(n|>GcBp?k z@)2Ch*Od;zr^$?_eA|LSN1lYGHJNa5x=@xN98O6;+|L}@3GcHeUNvNY1!^QEPs5|@ zp-0lvf{^{ej60eZSqMFrmI{Pp>DY7`R*sj*BKYHud>fkn0s*aorTj4HDyR#N6vL(C z@aV_z>35)M8Ug(#EZv5XJ__}@Bkgc$H$2KsqJM#=|3N?>fu&C(r0q}>(IMy3UGV5> z_;eVWE{ueWv<{+T&z|#LBqzj-fMhB`(&%mR{;L{Rl z`T_!40ZVz@&}&eWX+DcT>Btx~{RRU1FIbvHNKZjsa-jw`Yo59hxdg(Jq%4hMnDh3UP;M|P*>B@ z`F=GePa(MK$S1IL5kk&zm!{JEPxVwvR>Dp>@*{+F3)HpfYAzjxN2}n|-$BzaA)uA8 zGz%d;26a7pMwdPhkMfpU;;$UpvFNTX80kpn-n6>=xBK7C#;NNGE)1_bvAlld zPnSdo>$D?-@aQ`Dv<8|UMnLmn=~;x-Z&2dMKN?(`Zt&=w249?5*71g&QX&%#0jDlC zSZR_a#upB{vhX8TPT#g~nWF@}vBleGNX=xDUm1W}Y58^yyWjH1A( zY{u2YZnmVeWsT8s4u4{TD2k1ZqS1s=EHx^Z@t$#WB_%b+k46(lN23RfjYdmN7{%X= z%46&_ZWL+bMbT#bC}tWRMTZHZ*kf!ILne&E8l_}N8ce7-T5eRn{6?mX&gVnyF=4(m Oa` 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 From cc77650e49732213ad2c33b604ec0c1090ba466f Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Fri, 25 Sep 2026 11:10:44 +0200 Subject: [PATCH 6/8] Use standard normal distribution portably --- .github/lsan.supp | 3 -- .github/workflows/build.yml | 1 - src/maxent_helper.cpp | 10 +---- test/regression/PROVENANCE.md | 13 +++--- test/regression/REFERENCE_CHANGES.md | 14 ++---- test/regression/compare.py | 40 ++++++++++++++++++ test/regression/reference/t_generate_err.h5 | Bin 27512 -> 33656 bytes .../reference/t_generate_err_seed.h5 | Bin 27512 -> 33656 bytes test/regression/test_generate.py | 27 ++++++++++++ 9 files changed, 80 insertions(+), 28 deletions(-) delete mode 100644 .github/lsan.supp diff --git a/.github/lsan.supp b/.github/lsan.supp deleted file mode 100644 index 086b036..0000000 --- a/.github/lsan.supp +++ /dev/null @@ -1,3 +0,0 @@ -# HDF5 1.10 on Ubuntu 24.04 retains attribute-read allocations at exit. -# Keep LeakSanitizer enabled while excluding this external-library leak. -leak:H5A__read diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4022898..9a3f076 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,7 +123,6 @@ jobs: - name: Test Maxent env: ASAN_OPTIONS: detect_leaks=1 - LSAN_OPTIONS: suppressions=${{ github.workspace }}/.github/lsan.supp UBSAN_OPTIONS: print_stacktrace=1 run: ctest --test-dir build/ci --output-on-failure - name: Upload test logs on failure diff --git a/src/maxent_helper.cpp b/src/maxent_helper.cpp index 672b2ad..406a8ad 100644 --- a/src/maxent_helper.cpp +++ b/src/maxent_helper.cpp @@ -384,14 +384,8 @@ vector_type MaxEntHelper::generateGaussNoise(vector_type data, vector_type err,s const int N = data.size(); vector_type data_noise(N); for(int i=0;i(rng()) + 0.5, -32); - const double u2 = std::ldexp(static_cast(rng()) + 0.5, -32); - const double z = std::sqrt(-2.0 * std::log(u1)) * std::cos(2.0 * M_PI * u2); - data_noise[i] = data[i] + err[i] * z; + std::normal_distribution s(data[i],err[i]); + data_noise[i] = s(rng); } return data_noise; } diff --git a/test/regression/PROVENANCE.md b/test/regression/PROVENANCE.md index 7c52fd1..132781f 100644 --- a/test/regression/PROVENANCE.md +++ b/test/regression/PROVENANCE.md @@ -65,12 +65,13 @@ generations were identical. `t_generate_err_seed.h5` was added after commit default-seed reference, so the case detects an ignored `SEED` parameter. Both references include `case.out.booterr.dat`. -On 2026-09-25 the two bootstrap references were regenerated on the same -AppleClang/libc++ system after replacing `std::normal_distribution` with an -explicit transform of `mt19937` output. This makes the seeded sequence -independent of the standard-library implementation. The component reference -was regenerated at the same time after making the symmetric normalization -grid's zero crossing exact; only `model_general_double_gaussian.txt` changed. +`std::normal_distribution` deliberately remains the system-library +implementation, so a seed reproduces results within one implementation but +does not promise identical samples across standard libraries. The regression +harness validates the stochastic columns statistically while keeping the +frequency and spectrum columns strict. The component reference was regenerated +on 2026-09-25 after making the symmetric normalization grid's zero crossing +exact; only `model_general_double_gaussian.txt` changed. ## Time-bosonic references updated (B7b, 2026-09-24) diff --git a/test/regression/REFERENCE_CHANGES.md b/test/regression/REFERENCE_CHANGES.md index de08339..5aa2a84 100644 --- a/test/regression/REFERENCE_CHANGES.md +++ b/test/regression/REFERENCE_CHANGES.md @@ -4,20 +4,14 @@ Every regeneration of files in `reference/` is logged here: date, commit, reason, affected cases, and the largest difference to the previous references (from `compare.py --report`). Newest entry first. -## 2026-09-25: cross-library deterministic bootstrap and zero crossing +## 2026-09-25: deterministic model zero crossing * **Source:** `modernize/step2.4`, before the CI portability-fix commit. -* **Reason:** `std::normal_distribution` does not specify its transformation, - so identical `mt19937` seeds produced different bootstrap samples with - libc++ and libstdc++. Maxent now performs an explicit Box-Muller transform. - The default-model integration grid now constructs a symmetric midpoint as +* **Reason:** the default-model integration grid now constructs a symmetric midpoint as exact zero, avoiding compiler-dependent FMA rounding at the discontinuous branch of the general double Gaussian. -* **Cases:** `t_generate_err`, `t_generate_err_seed`, and `components`. - Only the two `booterr.dat` datasets and - `model_general_double_gaussian.txt` changed. -* **Largest differences:** 3.742e-2 relative in bootstrap output and 4.077e-4 - relative in the general-double-Gaussian component. +* **Cases:** `components`; only `model_general_double_gaussian.txt` changed. +* **Largest difference:** 4.077e-4 relative. ## 2026-09-24: explicit bootstrap seed coverage diff --git a/test/regression/compare.py b/test/regression/compare.py index aa27e2b..ddccd3c 100644 --- a/test/regression/compare.py +++ b/test/regression/compare.py @@ -30,6 +30,7 @@ HERE = Path(__file__).resolve().parent COMPARED = re.compile(r"^(files|log/scalars|cli)/") +BOOTSTRAP_DATASET = "files/case.out.booterr.dat" def load_tolerances(path): @@ -60,6 +61,33 @@ def as_text(x): return x.decode() if isinstance(x, bytes) else x +def validate_bootstrap(reference, result): + """Validate stochastic bootstrap output without fixing a library's PRNG transform.""" + a, b = np.asarray(reference, dtype=float), np.asarray(result, dtype=float) + problems = [] + if a.shape != b.shape or b.ndim != 2 or b.shape[1] != 4: + return [f"booterr shape {b.shape}, expected {a.shape} with four columns"] + if not np.all(np.isfinite(b)): + problems.append("booterr contains non-finite values") + return problems + if not np.array_equal(a[:, :2], b[:, :2]): + problems.append("booterr frequency or spectrum columns differ") + if np.any(b[:, 2:] < 0): + problems.append("booterr mean or error estimate is negative") + + spectrum_scale = float(np.max(np.abs(b[:, 1]), initial=0.0)) + if spectrum_scale == 0: + problems.append("booterr spectrum is identically zero") + return problems + mean_deviation = float(np.max(np.abs(b[:, 2] - b[:, 1]), initial=0.0)) + if mean_deviation > 0.25 * spectrum_scale: + problems.append("booterr bootstrap mean is inconsistent with the spectrum") + max_error = float(np.max(b[:, 3], initial=0.0)) + if max_error == 0 or max_error > 5.0 * spectrum_scale: + problems.append("booterr uncertainty estimate is zero or implausibly large") + return problems + + def compare_case(ref_path, res_path, rules, report): case = ref_path.stem problems, rows = [], [] @@ -83,6 +111,9 @@ def compare_case(ref_path, res_path, rules, report): problems.append(f"{case}: {name} text differs") continue a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float) + if name == BOOTSTRAP_DATASET: + problems.extend(f"{case}: {p}" for p in validate_bootstrap(a, b)) + continue if a.shape != b.shape: problems.append(f"{case}: {name} shape {b.shape}, reference {a.shape}") continue @@ -140,6 +171,15 @@ def ref_set(path): for p in problems: print(f" {p}") failed += bool(problems) + bootstrap_results = [args.result / f"{case}.h5" + for case in ("t_generate_err", "t_generate_err_seed")] + if not args.report and all(path.exists() for path in bootstrap_results): + with h5py.File(bootstrap_results[0], "r") as first, h5py.File(bootstrap_results[1], "r") as second: + default = first[BOOTSTRAP_DATASET][()][:, 2:] + explicit = second[BOOTSTRAP_DATASET][()][:, 2:] + if np.array_equal(default, explicit): + print("FAIL bootstrap seeds: default and explicit seeds produced identical estimates") + failed += 1 if not args.report: print(f"{len(refs)} cases, {failed} failed") return 1 if failed else 0 diff --git a/test/regression/reference/t_generate_err.h5 b/test/regression/reference/t_generate_err.h5 index 399bccfcc09c61b682cfdee77ce85202b6ef60b8..3f5710a19354962194532cb33d7f84ebfd0d21f8 100644 GIT binary patch delta 1947 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 delta 1324 zcmX|BeLU269M6f#L#dc3HkI%G{(N8G&*%N9N#tmxTInma_!%~?S_YND zb)2lDe2XeK?GY`V4bkKOrcp4e86!+3}Lpm zXttvQe)fhrUc!D_?Ky*8e?^S=4pm5)Cxe@NTfB7h6$sP2t2f~!r`eCs21V~;5X!4K zxG+Kt@rz9vLnr>kOtpB-<+Y3sw}`8>IxB%`k7(IZM+u%4y02bw;wk)v)d|6+9dwzm z{HNFRsWzydJGE?Yeh4myy_$DP35Vx?+-l25bWYh;x0$t%@DVe5oaZWqu&Q67kq*Lh z@mgN6nS{16N@;i;(2g_rD-D8pohS&k{y~>JgfRB|^tr10v}JD5{gsb9aP6L4^eVLz zF&o7f4o3`w^{kiQ^SDRXP4B(%fZEW7!^h*g)Vf_*a*@jV-A)Y|-|p5)y+*nhul;sYv%um(|I(nBF+B)aUELHp zF^c#--Mzw)a#|T^8P(LpLRKGVaZJNvYb;f%*sp?ljFzMOoQms(T&-POte-_0>Or`|qrm!z6$ygm{DS z4Zt+s18SGUL z`MZ-xi9{e0O?$6>SCmi&?Ze)pj94|%7;iP6l=!Bt(xd56C!FWiM8>lt#3Qhzrc7cc z(%2h~cHm0nu=Jqq2r(Im$$YiWwSKs$9WB1z+fCwzh)F?AT1$^}FF}ex`$sB)k&3t+ z?_Ayn^XnDmUzM~WVZgx5cK!fK2`Cbi@$2m#j Date: Fri, 25 Sep 2026 11:30:03 +0200 Subject: [PATCH 7/8] Address CI cache and regression review --- .github/workflows/build.yml | 11 ++++++++++- test/regression/compare.py | 22 +++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a3f076..32cfdf3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,12 +153,21 @@ jobs: brew install boost eigen hdf5 open-mpi python -m pip install --upgrade pip python -m pip install "numpy>=1.26" "h5py>=3.10" + - name: Determine dependency cache identity + id: dependency-cache + run: | + { + /usr/bin/clang --version + brew list --versions boost eigen hdf5 open-mpi + } > "${RUNNER_TEMP}/dependency-versions.txt" + dependency_key=$(shasum -a 256 "${RUNNER_TEMP}/dependency-versions.txt" | cut -d ' ' -f 1) + echo "key=${dependency_key}" >> "${GITHUB_OUTPUT}" - name: Cache ALPSCore id: cache-alpscore uses: actions/cache@v6 with: path: ${{ runner.temp }}/alpscore-install - key: maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-appleclang-${{ hashFiles('alpscore/**') }} + key: maxent-alpscore-${{ runner.os }}-${{ runner.arch }}-${{ steps.dependency-cache.outputs.key }}-${{ hashFiles('alpscore/**') }} - name: Build ALPSCore if: steps.cache-alpscore.outputs.cache-hit != 'true' env: diff --git a/test/regression/compare.py b/test/regression/compare.py index ddccd3c..35c87c9 100644 --- a/test/regression/compare.py +++ b/test/regression/compare.py @@ -171,15 +171,19 @@ def ref_set(path): for p in problems: print(f" {p}") failed += bool(problems) - bootstrap_results = [args.result / f"{case}.h5" - for case in ("t_generate_err", "t_generate_err_seed")] - if not args.report and all(path.exists() for path in bootstrap_results): - with h5py.File(bootstrap_results[0], "r") as first, h5py.File(bootstrap_results[1], "r") as second: - default = first[BOOTSTRAP_DATASET][()][:, 2:] - explicit = second[BOOTSTRAP_DATASET][()][:, 2:] - if np.array_equal(default, explicit): - print("FAIL bootstrap seeds: default and explicit seeds produced identical estimates") - failed += 1 + bootstrap_cases = {"t_generate_err", "t_generate_err_seed"} + selected_cases = {ref.stem for ref in refs} + bootstrap_results = [args.result / f"{case}.h5" for case in sorted(bootstrap_cases)] + if (not args.report and bootstrap_cases <= selected_cases + and all(path.exists() for path in bootstrap_results)): + with (h5py.File(bootstrap_results[0], "r") as first, + h5py.File(bootstrap_results[1], "r") as second): + if BOOTSTRAP_DATASET in first and BOOTSTRAP_DATASET in second: + default = first[BOOTSTRAP_DATASET][()][:, 2:] + explicit = second[BOOTSTRAP_DATASET][()][:, 2:] + if np.array_equal(default, explicit): + print("FAIL bootstrap seeds: default and explicit seeds produced identical estimates") + failed += 1 if not args.report: print(f"{len(refs)} cases, {failed} failed") return 1 if failed else 0 From 67d8bcf524819ee4bdfe0cb9ccca90d8752e10af Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Fri, 25 Sep 2026 11:47:13 +0200 Subject: [PATCH 8/8] Key Linux cache by exact toolchain --- .github/workflows/build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32cfdf3..dc471a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,16 @@ jobs: fi python -m pip install --upgrade pip python -m pip install "numpy>=1.26" "h5py>=3.10" + - name: Determine dependency cache identity + id: dependency-cache + run: | + { + "${{ matrix.plat.cxx_compiler }}-${{ matrix.plat.c_version }}" --version + dpkg-query -W -f='${Package}=${Version}\n' \ + libhdf5-serial-dev libeigen3-dev libopenmpi-dev + } > "${RUNNER_TEMP}/dependency-versions.txt" + dependency_key=$(sha256sum "${RUNNER_TEMP}/dependency-versions.txt" | cut -d ' ' -f 1) + echo "key=${dependency_key}" >> "${GITHUB_OUTPUT}" - name: Cache Boost and ALPSCore id: cache-dependencies uses: actions/cache@v6 @@ -76,7 +86,7 @@ jobs: path: | ${{ runner.temp }}/boost ${{ runner.temp }}/alpscore-install - key: maxent-deps-${{ runner.os }}-${{ runner.arch }}-${{ matrix.plat.c_compiler }}-${{ matrix.plat.c_version }}-boost-${{ env.BOOST_VERSION }}-${{ hashFiles('alpscore/**') }} + key: maxent-deps-${{ runner.os }}-${{ runner.arch }}-${{ steps.dependency-cache.outputs.key }}-boost-${{ env.BOOST_VERSION }}-${{ hashFiles('alpscore/**') }} - name: Build Boost and ALPSCore if: steps.cache-dependencies.outputs.cache-hit != 'true' env: