diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dc471a6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,214 @@ +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: 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 + with: + path: | + ${{ runner.temp }}/boost + ${{ runner.temp }}/alpscore-install + 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: + 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: 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 }}-${{ steps.dependency-cache.outputs.key }}-${{ 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)" \ + -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 + - 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/CMakeLists.txt b/CMakeLists.txt index 38ba864..81acdb0 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) @@ -55,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/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). 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.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) { diff --git a/test/regression/PROVENANCE.md b/test/regression/PROVENANCE.md index 8f3e1b7..132781f 100644 --- a/test/regression/PROVENANCE.md +++ b/test/regression/PROVENANCE.md @@ -65,6 +65,14 @@ 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`. +`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) 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..5aa2a84 100644 --- a/test/regression/REFERENCE_CHANGES.md +++ b/test/regression/REFERENCE_CHANGES.md @@ -4,6 +4,15 @@ 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: deterministic model zero crossing + +* **Source:** `modernize/step2.4`, before the CI portability-fix commit. +* **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:** `components`; only `model_general_double_gaussian.txt` changed. +* **Largest difference:** 4.077e-4 relative. + ## 2026-09-24: explicit bootstrap seed coverage * **Source:** `modernize/step2.3c` after commit `8b2ff05`. diff --git a/test/regression/compare.py b/test/regression/compare.py index aa27e2b..35c87c9 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,19 @@ def ref_set(path): for p in problems: print(f" {p}") failed += bool(problems) + 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 diff --git a/test/regression/reference/components.h5 b/test/regression/reference/components.h5 index 088f09a..3b10815 100644 Binary files a/test/regression/reference/components.h5 and b/test/regression/reference/components.h5 differ diff --git a/test/regression/test_generate.py b/test/regression/test_generate.py index 25f62d7..eb261c8 100644 --- a/test/regression/test_generate.py +++ b/test/regression/test_generate.py @@ -3,6 +3,9 @@ import unittest +import numpy as np + +from compare import validate_bootstrap from generate import classify_status @@ -28,5 +31,29 @@ def test_utility_nonzero_exit_is_failure(self): self.assertEqual(classify_status("kk", 1, "Caught Exception: bad input\n"), "failed") +class BootstrapValidationTest(unittest.TestCase): + def setUp(self): + omega = np.linspace(-2, 2, 5) + spectrum = np.array([0.1, 0.2, 0.4, 0.2, 0.1]) + self.reference = np.column_stack((omega, spectrum, spectrum * 1.01, + np.full(5, 0.05))) + + def test_accepts_different_reasonable_samples(self): + result = self.reference.copy() + result[:, 2] *= 0.98 + result[:, 3] *= 1.2 + self.assertEqual(validate_bootstrap(self.reference, result), []) + + def test_rejects_changed_deterministic_columns(self): + result = self.reference.copy() + result[0, 0] += 0.1 + self.assertIn("frequency or spectrum", " ".join(validate_bootstrap(self.reference, result))) + + def test_rejects_invalid_uncertainty(self): + result = self.reference.copy() + result[:, 3] = -1 + self.assertIn("negative", " ".join(validate_bootstrap(self.reference, result))) + + if __name__ == "__main__": unittest.main()