diff --git a/.github/workflows/Fortran-tests-linux.yml b/.github/workflows/Fortran-tests-linux.yml index 95e0265cb..299914e0c 100644 --- a/.github/workflows/Fortran-tests-linux.yml +++ b/.github/workflows/Fortran-tests-linux.yml @@ -44,6 +44,7 @@ jobs: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DKRYLOV_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ -DMUMPS_COMMON_LIBRARY=${{github.workspace}}/dependencies/lib/libmumps_common.a \ @@ -51,6 +52,7 @@ jobs: -DMUMPS_MPISEQ_LIBRARY=${{github.workspace}}/dependencies/lib/libmpiseq.a \ -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ + -DKRYLOV=${{github.workspace}}/dependencies/lib/libkrylov.so \ -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libblas.a;${{github.workspace}}/dependencies/lib/libcblas.a" \ -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ -DBUILD_STATIC_LIBS=ON \ @@ -61,7 +63,7 @@ jobs: - name: Compile Fortran tests working-directory: ${{github.workspace}}/interfaces/Fortran - run: gfortran example_uno.f90 -o example_uno -L../../build -luno -L${{github.workspace}}/dependencies/lib -lhighs -ldmumps -lmumps_common -lmpiseq -lpord -lmetis -lbqpd -llapack -lcblas -lblas -lstdc++ + run: gfortran example_uno.f90 -o example_uno -L../../build -luno -L${{github.workspace}}/dependencies/lib -lhighs -ldmumps -lmumps_common -lmpiseq -lpord -lmetis -lbqpd -lkrylov -llapack -lcblas -lblas -lstdc++ - name: Run Fortran tests working-directory: ${{github.workspace}}/interfaces/Fortran diff --git a/.github/workflows/build-python-wheels.yml b/.github/workflows/build-python-wheels.yml index abc82232b..14e5d2b3e 100644 --- a/.github/workflows/build-python-wheels.yml +++ b/.github/workflows/build-python-wheels.yml @@ -26,7 +26,9 @@ concurrency: cancel-in-progress: true env: - CIBW_TEST_COMMAND: python {project}/interfaces/Python/example/example_hs015.py + CIBW_TEST_COMMAND: > + python {project}/interfaces/Python/tests/run_wheel_tests.py + CIBW_TEST_REQUIRES: numpy jobs: build_wheels: @@ -52,6 +54,11 @@ jobs: with: python-version: "3.11" + - name: Install unzip (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: C:/msys64/usr/bin/bash -lc "pacman -Sy --noconfirm --needed unzip" + - run: pip install cibuildwheel if: runner.os != 'Windows' @@ -72,17 +79,41 @@ jobs: echo "LIBGFORTRAN_FOLDER=$LIBGFORTRAN_FOLDER" >> $GITHUB_ENV echo "OPENMP_LIB=$(brew --prefix libomp)/lib/libomp.dylib" >> $GITHUB_ENV + - name: Install matching MinGW (GCC 14.2.0 posix-seh-msvcrt) + if: runner.os == 'Windows' + shell: pwsh + run: | + $url = "https://github.com/niXman/mingw-builds-binaries/releases/download/14.2.0-rt_v12-rev0/x86_64-14.2.0-release-posix-seh-msvcrt-rt_v12-rev0.7z" + Invoke-WebRequest $url -OutFile mingw.7z + 7z x mingw.7z -oC:\mingw1420 -y # clean dir, NOT C:\mingw64 + # the archive contains a top-level mingw64\ folder, so the compiler ends up at: + C:\mingw1420\mingw64\bin\gcc --version + C:\mingw1420\mingw64\bin\g++ --version + + - name: Put 14.2 toolchain first on PATH (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: echo "C:\mingw1420\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Append + - name: Build wheels (Linux) if: runner.os == 'Linux' run: cibuildwheel --output-dir ${{github.workspace}}/wheelhouse env: CIBW_BUILD: ${{ matrix.sys.build }} - CIBW_SKIP: cp*-*_i686 *-win32 - CIBW_ENVIRONMENT_LINUX: + CIBW_SKIP: cp38-* *_i686 *-win32 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_34 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_34 + CIBW_ENVIRONMENT_LINUX: > CMAKE_GENERATOR=Ninja CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) + LD_LIBRARY_PATH=/project/dependencies/lib:/project/dependencies/lib/julia + AUDITWHEEL_PLAT=manylinux_2_35_$(uname -m) + # Linux wheels need glibc 2.35; test them in a separate job + CIBW_TEST_SKIP: "*-manylinux_*" CIBW_BEFORE_ALL_LINUX: bash dependencies/scripts/download_dependencies.sh + CIBW_REPAIR_WHEEL_COMMAND_LINUX: > + auditwheel repair --exclude libkrylov.so --exclude 'libjulia*.so*' --exclude 'libopenlibm*.so*' -w {dest_dir} {wheel} - name: Build wheels (macOS) if: startsWith(matrix.sys.os, 'macos') @@ -114,6 +145,10 @@ jobs: CMAKE_TOOLCHAIN_FILE=/tmp/clang_toolchain.cmake CMAKE_GENERATOR=Ninja CMAKE_BUILD_PARALLEL_LEVEL=$(sysctl -n hw.logicalcpu) + CIBW_CONFIG_SETTINGS_MACOS: > + cmake.define.KRYLOV=dependencies/lib/libkrylov.dylib + CIBW_REPAIR_WHEEL_COMMAND_MACOS: > + delocate-wheel --require-archs {delocate_archs} --ignore-missing-dependencies -w {dest_dir} -v {wheel} - name: Build wheels (Windows) if: runner.os == 'Windows' @@ -121,29 +156,67 @@ jobs: shell: pwsh env: CIBW_SKIP: cp314t-* cp*-*_i686 *-win32 - CIBW_BEFORE_BUILD_WINDOWS: > + CIBW_BEFORE_ALL_WINDOWS: > C:/msys64/usr/bin/bash -lc "cd \"$(cygpath -u '{package}')\" && ./dependencies/scripts/download_dependencies.sh" && pwsh -Command "Get-ChildItem -Path '{package}/dependencies' -Recurse | ForEach-Object { $_.LastWriteTime = Get-Date }" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: > pwsh -Command "$env:SOURCE_DATE_EPOCH='315532800'; - delvewheel repair --add-path 'C:/msys64/mingw64/bin;dependencies/bin;dependencies/lib' -w {dest_dir} {wheel}" + delvewheel repair --no-mangle-all --add-path 'dependencies/bin;dependencies/lib' -w {dest_dir} {wheel}" CIBW_ENVIRONMENT_WINDOWS: > CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) CMAKE_GENERATOR="MinGW Makefiles" - CMAKE_MAKE_PROGRAM=C:/mingw64/bin/mingw32-make.exe - CMAKE_C_COMPILER=C:/mingw64/bin/gcc.exe - CMAKE_CXX_COMPILER=C:/mingw64/bin/g++.exe - CMAKE_Fortran_COMPILER=C:/mingw64/bin/gfortran.exe + CMAKE_MAKE_PROGRAM=C:/mingw1420/mingw64/bin/mingw32-make.exe + CMAKE_C_COMPILER=C:/mingw1420/mingw64/bin/gcc.exe + CMAKE_CXX_COMPILER=C:/mingw1420/mingw64/bin/g++.exe + CMAKE_Fortran_COMPILER=C:/mingw1420/mingw64/bin/gfortran.exe CIBW_CONFIG_SETTINGS_WINDOWS: > cmake.define.METIS_LIBRARY=dependencies/bin/libmetis.dll + cmake.define.KRYLOV=dependencies/bin/libkrylov.dll cmake.define.AUXILIARY_LIBRARIES=dependencies/lib/libstdc++.a - name: Store artifacts uses: actions/upload-artifact@v4 + if: success() || failure() with: name: cibw-wheels-${{ matrix.sys.os }}-${{ strategy.job-index }} - path: wheelhouse/*.whl + path: ${{ github.workspace }}/wheelhouse/*.whl + if-no-files-found: warn + + # test the Linux wheels with glibc ≥ 2.35 + test-linux-wheels: + name: Test Linux wheels (${{ matrix.runs-on }}) + needs: build_wheels + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: + - runs-on: ubuntu-24.04 + pattern: cibw-wheels-ubuntu-latest-* + - runs-on: ubuntu-24.04-arm + pattern: cibw-wheels-ubuntu-22.04-arm-* + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: ${{ matrix.pattern }} + path: wheelhouse + merge-multiple: true + + - uses: actions/setup-python@v5 + with: { python-version: '3.11' } + + - name: Run tests + shell: bash + run: | + ls -R wheelhouse || { echo "no wheels downloaded"; exit 1; } + python -m pip install numpy + python -m pip install --no-index --find-links "$GITHUB_WORKSPACE/wheelhouse" --only-binary=:all: unopy + cd "$RUNNER_TEMP" # run from outside the repo so 'import unopy' hits the wheel, not the source + python "$GITHUB_WORKSPACE/interfaces/Python/tests/run_wheel_tests.py" build_sdist: name: Build source distribution @@ -275,4 +348,4 @@ jobs: - name: Run example working-directory: ${{github.workspace}}/interfaces/Python/example - run: python example_hs015.py \ No newline at end of file + run: python example_hs015.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f87c901d..8965591db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,6 +192,7 @@ jobs: -DCMAKE_FORTRAN_COMPILER=gfortran \ -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DKRYLOV_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ @@ -201,6 +202,7 @@ jobs: -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DKRYLOV=${{github.workspace}}/dependencies/lib/libkrylov.so \ -DBUILD_STATIC_LIBS=ON \ -DBUILD_SHARED_LIBS=ON @@ -214,6 +216,7 @@ jobs: -DCMAKE_FORTRAN_COMPILER=ifx \ -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DKRYLOV_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ @@ -223,6 +226,7 @@ jobs: -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DKRYLOV=${{github.workspace}}/dependencies/lib/libkrylov.so \ -DBUILD_STATIC_LIBS=ON \ -DBUILD_SHARED_LIBS=ON diff --git a/.github/workflows/unit-tests-asan-ubsan.yml b/.github/workflows/unit-tests-asan-ubsan.yml new file mode 100644 index 000000000..67ab0cd37 --- /dev/null +++ b/.github/workflows/unit-tests-asan-ubsan.yml @@ -0,0 +1,207 @@ +name: Unit tests with ASAN/UBSAN + +on: + push: + branches: [ "main" ] + paths-ignore: + - '*.md' + - 'LICENSE' + - '*.cff' + - '*.yml' + - '*.yaml' + - 'docs/**' + pull_request: + branches: [ "main" ] + paths-ignore: + - '*.md' + - 'LICENSE' + - '*.cff' + - '*.yml' + - '*.yaml' + - 'docs/**' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + + # ---------- LINUX ---------- + - os: ubuntu-latest + toolchain: gcc + build_type: Debug + shell: bash + + - os: ubuntu-24.04-arm + toolchain: gcc + build_type: Debug + shell: bash + + # ---------- MACOS ---------- + - os: macos-15 + toolchain: gcc + build_type: Debug + shell: bash + + - os: macos-15-intel + toolchain: gcc + build_type: Debug + shell: bash + + # ---------- WINDOWS MINGW ---------- + - os: windows-latest + toolchain: mingw + build_type: Debug + shell: msys2 {0} + + env: + BUILD_TYPE: ${{ matrix.build_type }} + + defaults: + run: + shell: ${{ matrix.shell }} + + steps: + - uses: actions/checkout@v4 + + # ========================================================= + # TOOLCHAIN SETUP + # ========================================================= + + # ----- macOS ----- + - name: Install libomp (macOS) + if: startsWith(matrix.os, 'macos') + run: | + brew install libomp + LIBGFORTRAN_FOLDER=$(dirname "$(gfortran-15 -print-file-name=libgfortran.dylib)") + echo "LIBGFORTRAN_FOLDER=$LIBGFORTRAN_FOLDER" >> $GITHUB_ENV + echo "OPENMP_LIB=$(brew --prefix libomp)/lib/libomp.dylib" >> $GITHUB_ENV + + # ----- MinGW ----- + - name: Setup MSYS2 + if: matrix.toolchain == 'mingw' + uses: msys2/setup-msys2@v2 + with: + path-type: inherit + msystem: MINGW64 + update: true + install: | + mingw-w64-x86_64-gcc-fortran + mingw-w64-x86_64-cmake + mingw-w64-x86_64-make + mingw-w64-x86_64-metis + + # ========================================================= + # DEPENDENCIES + # ========================================================= + + - name: Download dependencies + run: bash dependencies/scripts/download_dependencies.sh + + # ========================================================= + # CONFIGURE + # ========================================================= + + - name: Configure (Linux with ASan + UBSan) + if: startsWith(matrix.os, 'ubuntu') + run: | + SANITIZER_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -O1 -g" + + echo "ASAN_OPTIONS=detect_leaks=1:abort_on_error=1" >> $GITHUB_ENV + echo "UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1" >> $GITHUB_ENV + + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DENABLE_TESTS=ON \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_Fortran_COMPILER=gfortran \ + -DCMAKE_C_FLAGS="${SANITIZER_FLAGS}" \ + -DCMAKE_CXX_FLAGS="${SANITIZER_FLAGS}" \ + -DCMAKE_EXE_LINKER_FLAGS="${SANITIZER_FLAGS}" \ + -DCMAKE_SHARED_LINKER_FLAGS="${SANITIZER_FLAGS}" \ + -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ + -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ + -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ + -DMUMPS_COMMON_LIBRARY=${{github.workspace}}/dependencies/lib/libmumps_common.a \ + -DMUMPS_PORD_LIBRARY=${{github.workspace}}/dependencies/lib/libpord.a \ + -DMUMPS_MPISEQ_LIBRARY=${{github.workspace}}/dependencies/lib/libmpiseq.a \ + -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ + -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ + -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DBUILD_STATIC_LIBS=OFF \ + -DBUILD_SHARED_LIBS=ON + + - name: Configure (Mac) + if: startsWith(matrix.os, 'macos') + run: | + cmake -B build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + -DENABLE_TESTS=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_Fortran_COMPILER=gfortran-15 \ + -DCMAKE_EXE_LINKER_FLAGS="-L${{env.LIBGFORTRAN_FOLDER}}" \ + -DCMAKE_SHARED_LINKER_FLAGS="-L${{env.LIBGFORTRAN_FOLDER}}" \ + -DOpenMP_C_FLAGS="-Xpreprocessor -fopenmp" \ + -DOpenMP_C_LIB_NAMES="omp" \ + -DOpenMP_CXX_FLAGS="-Xpreprocessor -fopenmp" \ + -DOpenMP_CXX_LIB_NAMES="omp" \ + -DOpenMP_omp_LIBRARY=${{ env.OPENMP_LIB }} \ + -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ + -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ + -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ + -DMUMPS_COMMON_LIBRARY=${{github.workspace}}/dependencies/lib/libmumps_common.a \ + -DMUMPS_PORD_LIBRARY=${{github.workspace}}/dependencies/lib/libpord.a \ + -DMUMPS_MPISEQ_LIBRARY=${{github.workspace}}/dependencies/lib/libmpiseq.a \ + -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ + -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ + -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_SHARED_LIBS=OFF + + - name: Configure (MinGW) + if: matrix.toolchain == 'mingw' + shell: msys2 {0} + run: | + cmake -G "MinGW Makefiles" \ + -B build \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DENABLE_TESTS=ON \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_Fortran_COMPILER=gfortran \ + -DMUMPS_INCLUDE_DIR=${GITHUB_WORKSPACE}/dependencies/include \ + -DMETIS_INCLUDE_DIR=/mingw64/include \ + -DBQPD=${GITHUB_WORKSPACE}/dependencies/lib/libbqpd.a \ + -DMETIS_LIBRARY=/mingw64/lib/libmetis.a \ + -DMUMPS_LIBRARY=${GITHUB_WORKSPACE}/dependencies/lib/libdmumps.a \ + -DMUMPS_COMMON_LIBRARY=${GITHUB_WORKSPACE}/dependencies/lib/libmumps_common.a \ + -DMUMPS_PORD_LIBRARY=${GITHUB_WORKSPACE}/dependencies/lib/libpord.a \ + -DMUMPS_MPISEQ_LIBRARY=${GITHUB_WORKSPACE}/dependencies/lib/libmpiseq.a \ + -DBLAS_LIBRARIES="${GITHUB_WORKSPACE}/dependencies/lib/libcblas.a;${GITHUB_WORKSPACE}/dependencies/lib/libblas.a" \ + -DLAPACK_LIBRARIES=${GITHUB_WORKSPACE}/dependencies/lib/liblapack.a \ + -DHIGHS=${GITHUB_WORKSPACE}/dependencies/lib/libhighs.a \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_SHARED_LIBS=OFF + + # ========================================================= + # BUILD + # ========================================================= + + - name: Build + run: cmake --build build --target run_unotest --config ${{env.BUILD_TYPE}} --parallel + + # ========================================================= + # TEST + # ========================================================= + + - name: Run tests + working-directory: build + run: ctest --output-on-failure -C ${{env.BUILD_TYPE}} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 09999c800..a0a993cf2 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -104,14 +104,9 @@ jobs: # CONFIGURE # ========================================================= - - name: Configure (Linux with ASan + UBSan) + - name: Configure (Linux) if: startsWith(matrix.os, 'ubuntu') run: | - SANITIZER_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all -O1 -g" - - echo "ASAN_OPTIONS=detect_leaks=1:abort_on_error=1" >> $GITHUB_ENV - echo "UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1" >> $GITHUB_ENV - cmake -B build \ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DENABLE_TESTS=ON \ @@ -124,6 +119,7 @@ jobs: -DCMAKE_SHARED_LINKER_FLAGS="${SANITIZER_FLAGS}" \ -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DKRYLOV_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ @@ -133,6 +129,7 @@ jobs: -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DKRYLOV=${{github.workspace}}/dependencies/lib/libkrylov.so \ -DBUILD_STATIC_LIBS=OFF \ -DBUILD_SHARED_LIBS=ON @@ -154,6 +151,7 @@ jobs: -DOpenMP_omp_LIBRARY=${{ env.OPENMP_LIB }} \ -DMUMPS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DMETIS_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ + -DKRYLOV_INCLUDE_DIR=${{github.workspace}}/dependencies/include \ -DBQPD=${{github.workspace}}/dependencies/lib/libbqpd.a \ -DMETIS_LIBRARY=${{github.workspace}}/dependencies/lib/libmetis.a \ -DMUMPS_LIBRARY=${{github.workspace}}/dependencies/lib/libdmumps.a \ @@ -163,6 +161,7 @@ jobs: -DBLAS_LIBRARIES="${{github.workspace}}/dependencies/lib/libcblas.a;${{github.workspace}}/dependencies/lib/libblas.a" \ -DLAPACK_LIBRARIES=${{github.workspace}}/dependencies/lib/liblapack.a \ -DHIGHS=${{github.workspace}}/dependencies/lib/libhighs.a \ + -DKRYLOV=${{github.workspace}}/dependencies/lib/libkrylov.dylib \ -DBUILD_STATIC_LIBS=ON \ -DBUILD_SHARED_LIBS=OFF @@ -179,6 +178,7 @@ jobs: -DCMAKE_Fortran_COMPILER=gfortran \ -DMUMPS_INCLUDE_DIR=${GITHUB_WORKSPACE}/dependencies/include \ -DMETIS_INCLUDE_DIR=/mingw64/include \ + -DKRYLOV_INCLUDE_DIR=${GITHUB_WORKSPACE}/dependencies/include \ -DBQPD=${GITHUB_WORKSPACE}/dependencies/lib/libbqpd.a \ -DMETIS_LIBRARY=/mingw64/lib/libmetis.a \ -DMUMPS_LIBRARY=${GITHUB_WORKSPACE}/dependencies/lib/libdmumps.a \ @@ -188,6 +188,7 @@ jobs: -DBLAS_LIBRARIES="${GITHUB_WORKSPACE}/dependencies/lib/libcblas.a;${GITHUB_WORKSPACE}/dependencies/lib/libblas.a" \ -DLAPACK_LIBRARIES=${GITHUB_WORKSPACE}/dependencies/lib/liblapack.a \ -DHIGHS=${GITHUB_WORKSPACE}/dependencies/lib/libhighs.a \ + -DKRYLOV=${GITHUB_WORKSPACE}/dependencies/bin/libkrylov.dll \ -DBUILD_STATIC_LIBS=ON \ -DBUILD_SHARED_LIBS=OFF @@ -202,6 +203,12 @@ jobs: # TEST # ========================================================= + - name: Set PATH (Windows) + if: matrix.toolchain == 'mingw' + shell: msys2 {0} + run: | + echo "PATH=${GITHUB_WORKSPACE}/dependencies/bin:$PATH" >> $GITHUB_ENV + - name: Run tests working-directory: build run: ctest --output-on-failure -C ${{env.BUILD_TYPE}} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1761a6668..2ee7be295 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -256,6 +256,26 @@ if(SPRAL) message(STATUS "Found SPRAL") endif() +# Krylov +find_library(KRYLOV krylov) +if(KRYLOV) + target_link_libraries(uno_dependencies INTERFACE ${KRYLOV}) + target_compile_definitions(uno_dependencies INTERFACE HAS_KRYLOV) + + get_filename_component(KRYLOV_DIRECTORY ${KRYLOV} DIRECTORY) + # The bundle ships krylov.h in an 'include' folder sibling to the library + # directory (lib/ on Linux/macOS, bin/ on Windows). + find_path(KRYLOV_INCLUDE_DIR krylov.h + HINTS "${KRYLOV_DIRECTORY}/../include" "${KRYLOV_DIRECTORY}") + target_include_directories(uno_dependencies SYSTEM INTERFACE ${KRYLOV_INCLUDE_DIR}) + + if(ENABLE_TESTS) + target_sources(run_unotest PRIVATE unotest/functional_tests/KrylovSolversTests.cpp) + endif() + + message(STATUS "Found Krylov") +endif() + # helper function: resolve a mixed list of -lflags and relative paths to absolute paths function(resolve_library_paths input_list output_list) set(result) @@ -383,10 +403,73 @@ if(SKBUILD) target_compile_definitions(unopy PRIVATE PYBIND11_DETAILED_ERROR_MESSAGES) endif() + # The installed extension loads libkrylov from a 'lib' subdirectory of the + # package. Point a relative RPATH there so @rpath/libkrylov.dylib (macOS) / + # libkrylov.so (Linux) resolves to unopy/lib/ at runtime. Windows has no + # RPATH; unopy/__init__.py adds unopy/lib via os.add_dll_directory(). + if(APPLE) + set_target_properties(unopy PROPERTIES INSTALL_RPATH "@loader_path/lib") + elseif(UNIX) + set_target_properties(unopy PROPERTIES INSTALL_RPATH "$ORIGIN/lib") + endif() + install(TARGETS unopy LIBRARY DESTINATION unopy # Linux/macOS: unopy/unopy.so RUNTIME DESTINATION unopy # Windows: unopy/unopy.pyd ) + + # ========================================================================= + # Bundle the Krylov + Julia runtime into the wheel, INTACT + # ========================================================================= + # juliac emits a *relocatable tree* that the repair tools (delocate/auditwheel/ + # delvewheel) cannot place correctly: libkrylov finds its Julia runtime through + # baked-in relative paths and runtime dlopen(), neither of which the repair + # tools follow. So we install the tree ourselves into unopy/lib on every + # platform; the difference is only the source layout. + # + # Unix (macOS/Linux): libkrylov carries rpaths assuming it lives in a directory + # named 'lib' with the public libjulia.* alongside it and the internal kernel in + # a 'julia/' sibling (@loader_path/../lib, @loader_path/../lib/julia; the $ORIGIN + # equivalents on Linux). We copy the whole lib/ tree verbatim so those resolve. + # + # Windows: juliac output is flat (libkrylov.dll and all Julia DLLs together in + # dependencies/bin, no rpath, no julia/ subdir). delvewheel can't vendor them + # (libkrylov isn't in the .pyd's traceable import graph, and Julia loads its + # kernel by dlopen), so we install the flat DLL set into unopy/lib and + # unopy/__init__.py adds that directory via os.add_dll_directory(). + if(KRYLOV AND NOT WIN32) + get_filename_component(KRYLOV_ABS "${KRYLOV}" ABSOLUTE) + get_filename_component(KRYLOV_DIR "${KRYLOV_ABS}" DIRECTORY) + + # Copy the ENTIRE dependency lib/ tree into unopy/lib, preserving structure: + # lib/libkrylov.dylib -> unopy/lib/libkrylov.dylib + # lib/libjulia.*.dylib -> unopy/lib/libjulia.*.dylib (Julia public) + # lib/julia/*.dylib -> unopy/lib/julia/*.dylib (Julia internal) + # This reproduces juliac's build-time layout exactly, so every one of + # libkrylov's baked-in rpaths resolves: @loader_path/../lib finds the public + # libjulia.*, @loader_path/../lib/julia finds the internal kernel. Installing + # only libkrylov + the julia/ subdir (as before) missed libjulia.*, which + # sits at the top of lib/, not inside lib/julia/. + # + # Static archives are excluded: they're linked into the extension at build + # time, not loaded at runtime, so they would only bloat the wheel. + install(DIRECTORY "${KRYLOV_DIR}/" + DESTINATION unopy/lib + USE_SOURCE_PERMISSIONS + PATTERN "*.a" EXCLUDE + PATTERN "*.lib" EXCLUDE) + elseif(KRYLOV AND WIN32) + get_filename_component(KRYLOV_ABS "${KRYLOV}" ABSOLUTE) + get_filename_component(KRYLOV_DIR "${KRYLOV_ABS}" DIRECTORY) + + # Flat layout: install every DLL sitting next to libkrylov.dll (the Julia + # kernel + Julia's own MinGW runtime + libkrylov itself) into unopy/lib. + # __init__.py puts unopy/lib on the DLL search path so the .pyd finds + # libkrylov and libkrylov's dlopen()s of the Julia kernel resolve. + install(DIRECTORY "${KRYLOV_DIR}/" + DESTINATION unopy/lib + FILES_MATCHING PATTERN "*.dll") + endif() endif() # ====================== diff --git a/dependencies/scripts/download_dependencies.sh b/dependencies/scripts/download_dependencies.sh index 52a077fc7..4694fa1e6 100755 --- a/dependencies/scripts/download_dependencies.sh +++ b/dependencies/scripts/download_dependencies.sh @@ -5,24 +5,56 @@ set -e OS_NAME="$(uname -s)" case "$OS_NAME" in Linux*) - if [[ "$CIBW_BUILD" == *musllinux* ]] || ldd --version 2>&1 | grep -qi musl; then - OS="linux-musl" + if ldd --version 2>&1 | grep -qi musl; then + echo "Unsupported OS: linux-musl" + exit 1 else OS="linux-gnu" + OS_KRYLOV="linux" + EXTENSION_KRYLOV="tar.gz" fi ;; - Darwin*) OS="apple-darwin";; - Windows*) OS="w64-mingw32";; - MINGW64_NT*) OS="w64-mingw32";; - MSYS_NT*) OS="w64-mingw32";; - *) echo "Unsupported OS: $OS_NAME"; exit 1;; + Darwin*) + OS="apple-darwin" + OS_KRYLOV="macos" + EXTENSION_KRYLOV="tar.gz" + ;; + Windows*|MINGW64_NT*|MSYS_NT*) + OS="w64-mingw32" + OS_KRYLOV="windows" + EXTENSION_KRYLOV="zip" + ;; + *) + echo "Unsupported OS: $OS_NAME" + exit 1 + ;; esac # detect architecture ARCH_NAME="$(uname -m)" case "$ARCH_NAME" in - x86_64|amd64) ARCH="x86_64";; - arm64|aarch64) ARCH="aarch64";; - *) echo "Unknown architecture '$ARCH_NAME'."; exit 1;; + x86_64|amd64) + ARCH="x86_64" + ARCH_KRYLOV="x86_64" + ;; + arm64|aarch64) + ARCH="aarch64"; + case "$OS_KRYLOV" in + linux*) + ARCH_KRYLOV="aarch64" + ;; + macos*) + ARCH_KRYLOV="arm64" + ;; + *) + echo "Unsupported aarch64 OS: $OS_KRYLOV" + exit 1 + ;; + esac + ;; + *) + echo "Unknown architecture '$ARCH_NAME'." + exit 1 + ;; esac # change directory @@ -48,6 +80,21 @@ curl -L -o UnoUtils.tar.gz "$ASSET_URL" tar -xzf UnoUtils.tar.gz pwd +# download Krylov.jl +VERSION="v0.10.8" +REPO="https://github.com/JuliaSmoothOptimizers/Krylov.jl/releases/download/${VERSION}" +ASSET_NAME="libkrylov-${OS_KRYLOV}-${ARCH_KRYLOV}.${EXTENSION_KRYLOV}" +ASSET_URL="${REPO}/${ASSET_NAME}" +echo "Downloading: ${ASSET_URL}" +if [[ "$OS_KRYLOV" == "windows" ]]; then + curl -L -o libkrylov.zip "$ASSET_URL" + unzip libkrylov.zip +else + curl -L -o libkrylov.tar.gz "$ASSET_URL" + tar -xzf libkrylov.tar.gz +fi +pwd + # delete unwanted directories # rm -rf lib/cmake/cblas* lib/cmake/lapack* lib/pkgconfig rm -rf lib/cmake lib/pkgconfig diff --git a/interfaces/Python/tests/run_wheel_tests.py b/interfaces/Python/tests/run_wheel_tests.py new file mode 100644 index 000000000..a7830ff2e --- /dev/null +++ b/interfaces/Python/tests/run_wheel_tests.py @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Charlie Vanaret +# Licensed under the MIT license. See LICENSE file in the project directory for details. + +import unopy + +# test libkrylov (raises on failure) +unopy.test_libkrylov() + +# run the hs015 example +import runpy, os +here = os.path.dirname(__file__) +runpy.run_path(os.path.join(here, "..", "example", "example_hs015.py"), run_name="__main__") \ No newline at end of file diff --git a/interfaces/Python/unopy.cpp b/interfaces/Python/unopy.cpp index acacb1812..b43301e01 100644 --- a/interfaces/Python/unopy.cpp +++ b/interfaces/Python/unopy.cpp @@ -5,6 +5,7 @@ #include #include "unopy.hpp" #include "Uno.hpp" +#include "ingredients/subproblem_solvers/Krylov/KrylovSolvers.hpp" namespace py = pybind11; @@ -22,6 +23,9 @@ namespace uno { module.doc() = description; module.def("current_uno_version", &Uno::current_version); +#ifdef HAS_KRYLOV + module.def("test_libkrylov", &test_krylov_solvers); +#endif define_Model(module); define_Result(module); diff --git a/interfaces/Python/unopy/__init__.py b/interfaces/Python/unopy/__init__.py index 1033e3d01..84fb96cf9 100644 --- a/interfaces/Python/unopy/__init__.py +++ b/interfaces/Python/unopy/__init__.py @@ -1,14 +1,22 @@ import os import sys -# load bundled DLLs from unopy.libs +# Bundled DLLs ship in two places: +# unopy/lib - libkrylov + the Julia runtime (installed by CMake) +# unopy.libs - libraries vendored by delvewheel (e.g. the Fortran runtime) basedir = os.path.dirname(__file__) +libdir = os.path.join(basedir, 'lib') subdir = os.path.join(basedir, '..', 'unopy.libs') -if os.name == 'nt' and os.path.isdir(subdir): - # Prepend to PATH so our DLLs are found before any system MinGW ones - os.environ['PATH'] = subdir + os.pathsep + os.environ.get('PATH', '') - os.add_dll_directory(subdir) + +if os.name == 'nt': + # Prepend to PATH so our DLLs win over any system MinGW ones. libdir is + # prepended last, so it lands first on PATH — Julia's libstdc++ should take + # precedence over the one delvewheel vendored from msys2. + for d in (subdir, libdir): + if os.path.isdir(d): + os.environ['PATH'] = d + os.pathsep + os.environ.get('PATH', '') + os.add_dll_directory(d) elif sys.platform == 'cygwin': - os.environ['PATH'] = os.pathsep.join((os.environ['PATH'], basedir, subdir)) + os.environ['PATH'] = os.pathsep.join((os.environ['PATH'], basedir, libdir, subdir)) from .unopy import * \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 16da582d1..1805bf22e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ license = "MIT" dependencies = [ "numpy", ] +requires-python = ">=3.9" [project.urls] Repository = "https://github.com/cvanaret/Uno" @@ -49,3 +50,6 @@ MUMPS_PORD_LIBRARY = "dependencies/lib/libpord.a" MUMPS_MPISEQ_LIBRARY = "dependencies/lib/libmpiseq.a" # HiGHS HIGHS = "dependencies/lib/libhighs.a" +# Krylov.jl +KRYLOV = "dependencies/lib/libkrylov.so" +KRYLOV_INCLUDE_DIR = "dependencies/include" \ No newline at end of file diff --git a/uno/ingredients/subproblem_solvers/Krylov/KrylovSolvers.hpp b/uno/ingredients/subproblem_solvers/Krylov/KrylovSolvers.hpp new file mode 100644 index 000000000..02427f905 --- /dev/null +++ b/uno/ingredients/subproblem_solvers/Krylov/KrylovSolvers.hpp @@ -0,0 +1,96 @@ +// Copyright (c) 2026 Charlie Vanaret +// Licensed under the MIT license. See LICENSE file in the project directory for details. + +#ifndef UNO_KRYLOVSOLVERS_H +#define UNO_KRYLOVSOLVERS_H + +#include +#include +#include +#include + +static void test_krylov_solvers() { + /* + * basic_cg.c — minimal example: solve a 5x5 SPD system with CG. + * A = tridiag(-1, 2, -1), b = [1, 0, 0, 0, 1]^T + * Expected output: + * Solved: yes niter: 3 time: ... + * x = [ 1.00 1.00 1.00 1.00 1.00 ] + */ + + #define N 5 + + /* Tridiagonal matrix stored as three diagonals for simplicity. */ + typedef struct { + int n; + double diag[N]; /* main diagonal */ + double off[N-1]; /* sub/super diagonal */ + } TriDiag; + + /* Matvec callback: y = A * x */ + const auto matvec_A = [](const void *xv, void *yv, void *userdata) { + const double *x = (const double *) xv; + double *y = (double *) yv; + const TriDiag *A = (const TriDiag *) userdata; + const int n = A->n; + + for (int i = 0; i < n; i++) { + y[i] = A->diag[i] * x[i]; + if (i > 0) y[i] += A->off[i-1] * x[i-1]; + if (i < n-1) y[i] += A->off[i] * x[i+1]; + } + }; + + /* Build A = tridiag(-1, 2, -1) */ + TriDiag A; + A.n = N; + for (int i = 0; i < N; i++) A.diag[i] = 2.0; + for (int i = 0; i < N-1; i++) A.off[i] = -1.0; + + /* Right-hand side */ + double b[N] = {1.0, 0.0, 0.0, 0.0, 1.0}; + + /* Solution buffer */ + double x[N]; + + /* ----------------------------------------------------------------------- + * Create workspace for CG, double precision, CPU + * --------------------------------------------------------------------- */ + void *ws = NULL; + int ret = krylov_workspace_create(KRYLOV_CG, N, N, KRYLOV_FLOAT64, KRYLOV_CPU, + NULL, /* workspace options (NULL = defaults) */ + &ws); + assert(ret == 0); + + /* ----------------------------------------------------------------------- + * Solve + * --------------------------------------------------------------------- */ + KrylovOptions opts = krylov_default_options(); + opts.atol = 1e-10; /* absolute tolerance */ + opts.rtol = 1e-10; /* relative tolerance */ + ret = krylov_solve(ws, + matvec_A, /* y = A*x */ + NULL, /* y = Aᴴ*x (CG doesn't need it) */ + NULL, /* matvec_M: left/centered preconditioner (none) */ + NULL, /* matvec_N: right preconditioner (none) */ + b, /* right-hand side b (size m) */ + NULL, /* c = NULL (CG only needs one RHS) */ + &A, /* userdata forwarded to matvec_A */ + &opts); /* solver options (NULL = all defaults) */ + assert(ret == 0); + + /* ----------------------------------------------------------------------- + * Retrieve results + * --------------------------------------------------------------------- */ + ret = krylov_get_x(ws, x, N); + assert(ret == 0); + + std::cout << "KrylovSolvers: CG solution (should be a vector of 1):"; + for (int i = 0; i < N; i++) { + std::cout << ' ' << x[i]; + } + std::cout << '\n'; + krylov_workspace_free(ws); +} + +#endif // UNO_KRYLOVSOLVERS_H \ No newline at end of file diff --git a/unotest/functional_tests/KrylovSolversTests.cpp b/unotest/functional_tests/KrylovSolversTests.cpp new file mode 100644 index 000000000..0686fda1a --- /dev/null +++ b/unotest/functional_tests/KrylovSolversTests.cpp @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Charlie Vanaret +// Licensed under the MIT license. See LICENSE file in the project directory for details. + +#include +#include "ingredients/subproblem_solvers/Krylov/KrylovSolvers.hpp" + +/* + * basic_cg.c — minimal example: solve a 5x5 SPD system with CG. + * + * A = tridiag(-1, 2, -1), b = [1, 0, 0, 0, 1]^T + * + * Compile (after building libkrylov with CMake): + * + * gcc -o basic_cg basic_cg.c -I../include -L../build -lkrylov -Wl,-rpath,../build + * + * Expected output: + * Solved: yes niter: 3 time: ... + * x = [ 1.00 1.00 1.00 1.00 1.00 ] + */ + +/* ------------------------------------------------------------------------- + * Problem data + * ------------------------------------------------------------------------- */ + +#define N 5 + +/* Tridiagonal matrix stored as three diagonals for simplicity. */ +typedef struct { + int n; + double diag[N]; /* main diagonal */ + double off[N-1]; /* sub/super diagonal */ +} TriDiag; + +/* Matvec callback: y = A * x */ +static void matvec_A(const void *xv, void *yv, void *userdata) { + const double *x = (const double *)xv; + double *y = (double *)yv; + const TriDiag *A = (const TriDiag *)userdata; + int n = A->n; + + for (int i = 0; i < n; i++) { + y[i] = A->diag[i] * x[i]; + if (i > 0) y[i] += A->off[i-1] * x[i-1]; + if (i < n-1) y[i] += A->off[i] * x[i+1]; + } +} + +TEST(Krylov, Example) { + /* Build A = tridiag(-1, 2, -1) */ + TriDiag A; + A.n = N; + for (int i = 0; i < N; i++) A.diag[i] = 2.0; + for (int i = 0; i < N-1; i++) A.off[i] = -1.0; + + /* Right-hand side */ + double b[N] = {1.0, 0.0, 0.0, 0.0, 1.0}; + + /* Solution buffer */ + double x[N]; + + /* ----------------------------------------------------------------------- + * Create workspace for CG, double precision, CPU + * --------------------------------------------------------------------- */ + void *ws = NULL; + int ret = krylov_workspace_create(KRYLOV_CG, N, N, KRYLOV_FLOAT64, KRYLOV_CPU, + NULL, /* workspace options (NULL = defaults) */ + &ws); + ASSERT_EQ(ret, 0); + + /* ----------------------------------------------------------------------- + * Solve + * --------------------------------------------------------------------- */ + KrylovOptions opts = krylov_default_options(); + opts.atol = 1e-10; /* absolute tolerance */ + opts.rtol = 1e-10; /* relative tolerance */ + ret = krylov_solve(ws, + matvec_A, /* y = A*x */ + NULL, /* y = Aᴴ*x (CG doesn't need it) */ + NULL, /* matvec_M: left/centered preconditioner (none) */ + NULL, /* matvec_N: right preconditioner (none) */ + b, /* right-hand side b (size m) */ + NULL, /* c = NULL (CG only needs one RHS) */ + &A, /* userdata forwarded to matvec_A */ + &opts); /* solver options (NULL = all defaults) */ + ASSERT_EQ(ret, 0); + + /* ----------------------------------------------------------------------- + * Retrieve results + * --------------------------------------------------------------------- */ + ret = krylov_get_x(ws, x, N); + ASSERT_EQ(ret, 0); + + const double tolerance = 1e-8; + for (int i = 0; i < N; i++) { + EXPECT_NEAR(x[i], 1., tolerance); + } + + /* ----------------------------------------------------------------------- + * Free workspace + * --------------------------------------------------------------------- */ + krylov_workspace_free(ws); +} \ No newline at end of file