diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 875c12124b..0521a488bf 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -8,15 +8,27 @@ on: branches: [main, release-*] jobs: - test: - name: Compile and test planner + compile: + name: Compile planner timeout-minutes: 60 - runs-on: ${{ matrix.version.macos }} strategy: matrix: version: - - {macos: macos-14, python: '3.14'} - - {macos: macos-15, python: '3.14'} + - {macos: macos-15, python: '3.10', run_tox_tests: true} + - {macos: macos-26, python: '3.10', run_tox_tests: true} + runs-on: ${{ matrix.version.macos }} + env: + GUROBI_URL: ${{ secrets.GUROBI1302_OSX_URL }} + GUROBI_LICENSE_URL: ${{ secrets.GUROBI_LICENSE_URL }} + GRB_LICENSE_FILE: /Users/runner/gurobi.lic + GUROBI_HOME: /Users/runner/lib/gurobi1302/macos_universal2 + DYLD_LIBRARY_PATH: /Users/runner/lib/gurobi1302/macos_universal2/lib:/Applications/CPLEX_Studio222/cplex/bin/arm64_osx + soplex_DIR: /Users/runner/lib/soplex-7.1.6 + SOPLEX_LIB: /Users/runner/lib/soplex-7.1.6/lib/ + SOPLEX_INCLUDE: /Users/runner/lib/soplex-7.1.6/include/ + CPLEX_URL: ${{ secrets.CPLEX2220_OSX_URL }} + cplex_DIR: /Applications/CPLEX_Studio222/cplex + CPLEX_LIB: /Applications/CPLEX_Studio222/cplex/bin/arm64_osx/libcplex2220.dylib steps: - name: Clone repository uses: actions/checkout@v3 @@ -26,33 +38,181 @@ jobs: with: python-version: ${{ matrix.version.python }} + - name: Install dependencies + run: | + brew install coreutils gmp + + - name: Install Gurobi + if: ${{ env.GUROBI_URL != 0 }} + run: | + wget -O "$RUNNER_TEMP/gurobi.pkg" "$GUROBI_URL" &> /dev/null + sudo installer -pkg "$RUNNER_TEMP/gurobi.pkg" -target / + mkdir -p "$HOME/lib/gurobi1302" + cp -R /Library/gurobi1302/macos_universal2 "$HOME/lib/gurobi1302/" + rm "$RUNNER_TEMP/gurobi.pkg" + test -f "$GUROBI_HOME/include/gurobi_c.h" + test -d "$GUROBI_HOME/lib" + + - name: Check Gurobi license + if: ${{ env.GUROBI_URL != 0 }} + run: | + wget -O "$GRB_LICENSE_FILE" "$GUROBI_LICENSE_URL" &> /dev/null + chmod 600 "$GRB_LICENSE_FILE" + if ! "$GUROBI_HOME/bin/gurobi_cl" --license &> /dev/null; then + echo "Gurobi license is invalid or expired. Please contact gustavo.delazeri@unibas.ch" + exit 1 + fi + echo "Gurobi license check passed." + + - name: Install SoPlex + run: | + git clone https://github.com/scipopt/soplex.git + cd soplex + git checkout release-716 + cd .. + cmake -S soplex -B build + cmake --build build + cmake --install build --prefix "${soplex_DIR}" + rm -rf soplex build + + - name: Install CPLEX + if: ${{ env.CPLEX_URL != 0 }} + run: | + wget -O "$RUNNER_TEMP/cplex.pkg" "$CPLEX_URL" &> /dev/null + sudo installer -pkg "$RUNNER_TEMP/cplex.pkg" -target / + rm "$RUNNER_TEMP/cplex.pkg" + test -f "${cplex_DIR}/include/ilcplex/cplex.h" + test -f "$CPLEX_LIB" + - name: Compile planner run: | - export CXXFLAGS="-Werror" # Treat compilation warnings as errors. - ./build.py + gmp_prefix="$(brew --prefix gmp)" + export CXXFLAGS="-Werror -I${gmp_prefix}/include" + export LDFLAGS="-L${gmp_prefix}/lib" ./build.py --debug + ./build.py + + - name: Archive required files + if: ${{ matrix.version.run_tox_tests }} + run: | + files_to_archive="fast-downward.py driver misc src builds/debug/bin/ builds/release/bin/ ${SOPLEX_LIB} ${SOPLEX_INCLUDE}" + if [[ -n "${GUROBI_URL}" ]]; then + files_to_archive="${files_to_archive} ${GUROBI_HOME}/lib" + fi + if [[ -n "${CPLEX_URL}" ]]; then + mkdir -p "$HOME/lib/cplex2220" + cp "$CPLEX_LIB" "$HOME/lib/cplex2220/" + files_to_archive="${files_to_archive} $HOME/lib/cplex2220/libcplex2220.dylib" + fi + tar czf archive.tar.gz -C "$HOME" $(grealpath --relative-to="$HOME" $files_to_archive) + + - name: Upload archive + if: ${{ matrix.version.run_tox_tests }} + uses: actions/upload-artifact@v4.4.0 + with: + name: compiled-planner-${{ matrix.version.macos }} + path: archive.tar.gz + retention-days: 1 + + run_tox_tests: + name: Test planner + needs: compile # TODO: this only depends on the compile step with the gcc version we test + strategy: + matrix: + version: + - {macos: macos-15, python: '3.10'} + - {macos: macos-26, python: '3.10'} + runs-on: ${{ matrix.version.macos }} + env: + GUROBI_URL: ${{ secrets.GUROBI1302_OSX_URL }} + GUROBI_LICENSE_URL: ${{ secrets.GUROBI_LICENSE_URL }} + GRB_LICENSE_FILE: /Users/runner/gurobi.lic + GUROBI_HOME: /Users/runner/lib/gurobi1302/macos_universal2 + DYLD_LIBRARY_PATH: /Users/runner/lib/gurobi1302/macos_universal2/lib:/Applications/CPLEX_Studio222/cplex/bin/arm64_osx + CPLEX_URL: ${{ secrets.CPLEX2220_OSX_URL }} + CPLEX_LIB: /Applications/CPLEX_Studio222/cplex/bin/arm64_osx/libcplex2220.dylib + CPLEX_ARCHIVE_LIB: /Users/runner/lib/cplex2220/libcplex2220.dylib + steps: + - name: Download archive + uses: actions/download-artifact@v4.1.8 + with: + name: compiled-planner-${{ matrix.version.macos }} - - name: Install tox + - name: Delete artifact (ignore if not found) + uses: geekyeggo/delete-artifact@v2 + continue-on-error: true + with: + name: compiled-planner-${{ matrix.version.macos }} + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.version.python }} + + - name: Install dependencies run: | pip3 install tox + brew install gmp - name: Install VAL run: | - brew install gnu-sed git clone https://github.com/KCL-Planning/VAL.git cd VAL git checkout a5565396007eee73ac36527fbf904142b3077c74 make clean # Remove old build artifacts and binaries. - gsed -i 's/-Werror //g' Makefile # Ignore warnings. + sed -i '' 's/-Werror //g' Makefile # Ignore warnings. make -j2 mv validate ../ cd ../ rm -rf VAL + echo "$PWD" >> "$GITHUB_PATH" # Add VAL to path of subsequent steps. + + - name: Extract archive + # We need to make sure that library paths are the same as + # during compilation. + run: | + tar xfz archive.tar.gz -C "$HOME" + + # Fast Downward's binary records Gurobi's installation path under /Library/gurobi1302/macos_universal2 + # This step copies the archived Gurobi to the right place + + - name: Restore Gurobi library path + if: ${{ env.GUROBI_URL != 0 }} + run: | + sudo mkdir -p /Library/gurobi1302 + sudo cp -R "$GUROBI_HOME" /Library/gurobi1302/ + test -f /Library/gurobi1302/macos_universal2/lib/libgurobi130.dylib + + # Ditto for cplex + - name: Restore CPLEX library path + if: ${{ env.CPLEX_URL != 0 }} + run: | + sudo mkdir -p "$(dirname "$CPLEX_LIB")" + sudo cp "$CPLEX_ARCHIVE_LIB" "$CPLEX_LIB" + sudo chmod 755 "$CPLEX_LIB" + test -r "$CPLEX_LIB" - name: Run driver, translator and search tests run: | - export PATH="$(pwd):$PATH" # Add VAL to path. - cd misc - tox -e driver,translator,search + cd misc/ + tox -e driver,translator,search,parameters,generate-docs -... + - name: Run CPLEX tests + if: ${{ env.CPLEX_URL != 0 }} + run: | + cd misc/ + tox -e cplex + + - name: Run Gurobi tests + if: ${{ env.GUROBI_URL != 0 }} + run: | + # We redirect output of wget to hide the secret URL. + wget -O "$GRB_LICENSE_FILE" "$GUROBI_LICENSE_URL" &> /dev/null + chmod 600 "$GRB_LICENSE_FILE" + cd misc/ + tox -e gurobi + + - name: Run SoPlex tests + run: | + cd misc/ + tox -e soplex diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index f3a5d9e668..7998206a9d 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -31,9 +31,18 @@ jobs: env: CC: ${{ matrix.version.cc }} CXX: ${{ matrix.version.cxx }} + CPLEX_URL: ${{ secrets.CPLEX2211_LINUX_URL }} cplex_DIR: /home/runner/lib/ibm/ILOG/CPLEX_Studio2211/cplex CPLEX_LIB: /home/runner/lib/ibm/ILOG/CPLEX_Studio2211/cplex/bin/x86-64_linux/libcplex2211.so + + GUROBI_URL: ${{ secrets.GUROBI1302_LINUX_URL }} + GUROBI_LICENSE_URL: ${{ secrets.GUROBI_LICENSE_URL }} + GRB_LICENSE_FILE: /home/runner/gurobi.lic + GUROBI_HOME: /home/runner/lib/gurobi1302/linux64 + GUROBI_LIB: /home/runner/lib/gurobi1302/linux64/lib + LD_LIBRARY_PATH: /home/runner/lib/gurobi1302/linux64/lib + soplex_DIR: /home/runner/lib/soplex-7.1.0 SOPLEX_LIB: /home/runner/lib/soplex-7.1.0/lib/ SOPLEX_INCLUDE: /home/runner/lib/soplex-7.1.0/include/ @@ -70,6 +79,27 @@ jobs: ./cplex_installer -DLICENSE_ACCEPTED=TRUE -DUSER_INSTALL_DIR="$(dirname "${cplex_DIR}")" -i silent rm cplex_installer + # Only install Gurobi if its URL/secret is set. + - name: Install Gurobi + if: ${{ env.GUROBI_URL != 0 }} + run: | + # We redirect output of wget to hide the secret URL. + wget -O gurobi.tar.gz $GUROBI_URL &> /dev/null + tar xfz gurobi.tar.gz -C /home/runner/lib + rm gurobi.tar.gz + + - name: Check Gurobi license + if: ${{ env.GUROBI_URL != 0 }} + run: | + # We redirect output of wget to hide the secret URL. + wget -O "$GRB_LICENSE_FILE" "$GUROBI_LICENSE_URL" &> /dev/null + chmod 600 "$GRB_LICENSE_FILE" + if ! "$GUROBI_HOME/bin/gurobi_cl" --license &> /dev/null; then + echo "Gurobi license is invalid or expired. Please contact gustavo.delazeri@unibas.ch" + exit 1 + fi + echo "Gurobi license check passed." + # Always install SoPlex - name: Install SoPlex run: | @@ -98,6 +128,9 @@ jobs: if [[ ! -z "${CPLEX_URL}" ]]; then files_to_archive="${files_to_archive} ${CPLEX_LIB}" fi + if [[ ! -z "${GUROBI_URL}" ]]; then + files_to_archive="${files_to_archive} ${GUROBI_LIB}" + fi tar cfz archive.tar.gz -C "/home/runner" $(realpath --relative-to="/home/runner" $files_to_archive) - name: Upload archive @@ -120,6 +153,10 @@ jobs: - {ubuntu: ubuntu-24.04, python: '3.10'} env: CPLEX_URL: ${{ secrets.CPLEX2211_LINUX_URL }} + GUROBI_URL: ${{ secrets.GUROBI1302_LINUX_URL }} + GUROBI_LICENSE_URL: ${{ secrets.GUROBI_LICENSE_URL }} + GRB_LICENSE_FILE: /home/runner/gurobi.lic + LD_LIBRARY_PATH: /home/runner/lib/gurobi1302/linux64/lib steps: - name: Download archive uses: actions/download-artifact@v4.1.8 @@ -172,6 +209,15 @@ jobs: cd misc/ tox -e cplex + - name: Run Gurobi tests + if: ${{ env.GUROBI_URL != 0 }} + run: | + # We redirect output of wget to hide the secret URL. + wget -O "$GRB_LICENSE_FILE" $GUROBI_LICENSE_URL &> /dev/null + chmod 600 "$GRB_LICENSE_FILE" + cd misc/ + tox -e gurobi + - name: Run SoPlex tests run: | cd misc/ diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index acf91841b3..c66ecf05eb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -15,6 +15,11 @@ env: cplex_DIR: D:\a\cplex CPLEX_URL: "${{ secrets.CPLEX2211_WINDOWS_URL }}" + GUROBI_URL: "${{ secrets.GUROBI1302_WINDOWS_URL }}" + GUROBI_LICENSE_URL: "${{ secrets.GUROBI_LICENSE_URL }}" + GUROBI_HOME: C:\gurobi1302\win64 + soplex_DIR: D:\a\soplex-7.1.6 + soplex_DEBUG_DIR: D:\a\soplex-7.1.6-debug ZLIB_URL: "https://www.zlib.net/zlib132.zip" @@ -45,6 +50,53 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Install Gurobi + if: ${{ env.GUROBI_URL != 0 }} + run: | + $installer = Join-Path $ENV:RUNNER_TEMP "gurobi.msi" + curl.exe --fail --location --silent --show-error --output "$installer" "$ENV:GUROBI_URL" + if ($LASTEXITCODE -ne 0) { + throw "Failed to download Gurobi." + } + + $arguments = "/i `"$installer`" /quiet /norestart" + $process = Start-Process -FilePath msiexec.exe -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -notin @(0, 3010)) { + throw "Gurobi installation failed with exit code $($process.ExitCode)." + } + Remove-Item "$installer" + + $required_files = @( + "$ENV:GUROBI_HOME\include\gurobi_c.h", + "$ENV:GUROBI_HOME\lib\gurobi130.lib", + "$ENV:GUROBI_HOME\bin\gurobi130.dll", + "$ENV:GUROBI_HOME\bin\gurobi_cl.exe" + ) + foreach ($file in $required_files) { + if (-not (Test-Path "$file")) { + throw "Required Gurobi file not found: $file" + } + } + + Add-Content -Path $ENV:GITHUB_PATH -Value "$ENV:GUROBI_HOME\bin" + + - name: Check Gurobi license + if: ${{ env.GUROBI_URL != 0 }} + run: | + $license_file = Join-Path $ENV:USERPROFILE "gurobi.lic" + curl.exe --fail --location --silent --show-error --output "$license_file" "$ENV:GUROBI_LICENSE_URL" + if ($LASTEXITCODE -ne 0) { + throw "Failed to download the Gurobi license." + } + Add-Content -Path $ENV:GITHUB_ENV -Value "GRB_LICENSE_FILE=$license_file" + $ENV:GRB_LICENSE_FILE = $license_file + + & "$ENV:GUROBI_HOME\bin\gurobi_cl.exe" --license *> $null + if ($LASTEXITCODE -ne 0) { + Write-Host "Gurobi license is invalid or expired. Please contact gustavo.delazeri@unibas.ch" + exit 1 + } + Write-Host "Gurobi license check passed." - name: Install zlib if: ${{ env.CPLEX_URL != 0 }} @@ -89,13 +141,37 @@ jobs: echo "Copy the relevant directory to a location which is not magically protected against CMake" Xcopy /E /I D:\a\cplex_temp\cplex $ENV:cplex_DIR + - name: Install SoPlex + shell: cmd + run: | + call "${{ matrix.platform.vc }}" %ARCH% + git clone --branch release-716 --depth 1 https://github.com/scipopt/soplex.git || exit /b 1 + cmake -S soplex -B soplex-build-release -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DBOOST=off -DGMP=off -DZLIB=off || exit /b 1 + cmake --build soplex-build-release || exit /b 1 + cmake --install soplex-build-release --prefix "%soplex_DIR%" || exit /b 1 + + cmake -S soplex -B soplex-build-debug -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug -DBOOST=off -DGMP=off -DZLIB=off || exit /b 1 + cmake --build soplex-build-debug || exit /b 1 + cmake --install soplex-build-debug --prefix "%soplex_DEBUG_DIR%" || exit /b 1 + + if not exist "%soplex_DIR%\include\soplex.h" ( + echo ERROR: SoPlex release headers were not installed. + exit /b 1 + ) + if not exist "%soplex_DEBUG_DIR%\include\soplex.h" ( + echo ERROR: SoPlex debug headers were not installed. + exit /b 1 + ) + echo %soplex_DIR%\bin>>"%GITHUB_PATH%" - name: Compile planner shell: cmd + # We set SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING because SoPlex 7.1.6 uses some deprecated Microsoft C++ extensions. run: | call "${{ matrix.platform.vc }}" %ARCH% - set CXXFLAGS=/WX + set CXXFLAGS=/WX /D_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING python build.py release + set soplex_DIR=%soplex_DEBUG_DIR% python build.py debug - name: Install tox @@ -122,4 +198,19 @@ jobs: cd misc/ tox -e cplex + - name: Run Gurobi tests + shell: cmd + if: ${{ env.GUROBI_URL != 0 }} + run: | + call "${{ matrix.platform.vc }}" %ARCH% + cd misc/ + tox -e gurobi + + - name: Run SoPlex tests + shell: cmd + run: | + call "${{ matrix.platform.vc }}" %ARCH% + cd misc/ + tox -e soplex + ... diff --git a/BUILD.md b/BUILD.md index f897124712..f5c2af1f0e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -16,9 +16,9 @@ During the installation of Visual Studio, the C++ compiler is not installed by d ### Optional: Linear-Programming Solvers -Some planner configurations depend on an LP or MIP solver. We support CPLEX (commercial, [free academic license](http://ibm.com/academic)) and SoPlex (Apache License, no MIP support). You can install one or both solvers without causing conflicts. +Some planner configurations depend on an LP or MIP solver. We support CPLEX (commercial, [free academic license](http://ibm.com/academic)), Gurobi (commercial, [free academic license](https://www.gurobi.com/academics#licenses)), and SoPlex (Apache License, no MIP support). You can install one or more solvers without causing conflicts. -Once LP solvers are installed and the environment variables `cplex_DIR` and/or `soplex_DIR` are set up correctly, Fast Downward automatically includes each solver detected on the system in the build. +Once LP solvers are installed and the appropriate environment variables are set up correctly, Fast Downward automatically includes each solver detected on the system in the build. #### Installing CPLEX @@ -32,6 +32,21 @@ export cplex_DIR=/opt/ibm/ILOG/CPLEX_Studio2211/cplex ``` Note that on Windows, setting up the environment variable might require using `/` instead of the more Windows-common `\`. +#### Installing Gurobi + +We currently support Gurobi 13.0.x. Download and install the appropriate Gurobi package for your operating system and obtain a license. + +Set `GUROBI_HOME` to the directory containing Gurobi's `include`, `lib`, and `bin` directories. Set `GRB_LICENSE_FILE` to the location of your Gurobi license file. For example, `GUROBI_HOME` could be `/opt/gurobi130x/linux64` on Ubuntu, `/Library/gurobi130x/macos_universal2` on macOS, or `C:\gurobi130x\win64` on Windows. + +On Ubuntu, add Gurobi's `lib` directory to `LD_LIBRARY_PATH`. On macOS, add it to `DYLD_LIBRARY_PATH`. On Windows, add Gurobi's `bin` directory to `PATH` so that `gurobi130.dll` can be found when running Fast Downward. + +For example, assuming that Gurobi 13.0.2 was installed in its default location and the license is stored in the user's home directory, run the following commands in PowerShell: + +```powershell +$env:GUROBI_HOME = "C:\gurobi1302\win64" +$env:GRB_LICENSE_FILE = "$env:USERPROFILE\gurobi.lic" +$env:PATH = "$env:GUROBI_HOME\bin;$env:PATH" +``` #### Installing SoPlex on Linux/macOS diff --git a/misc/style/run-clang-tidy.py b/misc/style/run-clang-tidy.py index 6a0fc95f28..abfefe70cf 100755 --- a/misc/style/run-clang-tidy.py +++ b/misc/style/run-clang-tidy.py @@ -19,6 +19,7 @@ IGNORES = [ "'cplex.h' file not found [clang-diagnostic-error]", "'soplex.h' file not found [clang-diagnostic-error]", + "'gurobi_c.h' file not found [clang-diagnostic-error]", "'git_revision.h' file not found [clang-diagnostic-error]", "local copy 'copied_key' of the variable 'key' is never modified; consider avoiding the copy [performance-unnecessary-copy-initialization]", ] diff --git a/misc/tests/test-standard-configs.py b/misc/tests/test-standard-configs.py index 412c71b174..a16a543767 100644 --- a/misc/tests/test-standard-configs.py +++ b/misc/tests/test-standard-configs.py @@ -64,6 +64,12 @@ def test_configs_cplex(config, debug): run_plan_script(SAS_FILE, config, debug) +@pytest.mark.parametrize("config", sorted(configs.configs_optimal_lp(lp_solver="gurobi").values())) +@pytest.mark.parametrize("debug", [False, True]) +def test_configs_gurobi(config, debug): + run_plan_script(SAS_FILE, config, debug) + + @pytest.mark.parametrize("config", sorted(configs.configs_optimal_lp(lp_solver="soplex").values())) @pytest.mark.parametrize("debug", [False, True]) def test_configs_soplex(config, debug): diff --git a/misc/tox.ini b/misc/tox.ini index be7908006c..ad4bf7c880 100644 --- a/misc/tox.ini +++ b/misc/tox.ini @@ -53,6 +53,13 @@ deps = commands = pytest test-standard-configs.py -k test_configs_cplex +[testenv:gurobi] +changedir = {toxinidir}/tests/ +deps = + pytest +commands = + pytest test-standard-configs.py -k test_configs_gurobi + [testenv:soplex] changedir = {toxinidir}/tests/ deps = diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index 701b6ab9ca..bc4961d776 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -601,18 +601,29 @@ create_fast_downward_library( if(USE_LP) find_package(Cplex 12) if(CPLEX_FOUND) + message(STATUS "Found CPLEX: ${cplex_DIR}") target_compile_definitions(lp_solver INTERFACE HAS_CPLEX) target_link_libraries(lp_solver INTERFACE cplex::cplex) target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) endif() + find_package(soplex 7.1.0 QUIET) if (SOPLEX_FOUND) - message(STATUS "Found SoPlex: ${SOPLEX_INCLUDE_DIRS}") + message(STATUS "Found SoPlex: ${soplex_DIR}") target_link_libraries(lp_solver INTERFACE libsoplex) target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) endif() + + find_package(Gurobi QUIET) + if(Gurobi_FOUND) + message(STATUS "Found Gurobi: ${GUROBI_LIBRARY} ${GUROBI_INCLUDE_DIR}") + target_compile_definitions(lp_solver INTERFACE HAS_GUROBI) + target_link_libraries(lp_solver INTERFACE gurobi::gurobi) + target_sources(lp_solver INTERFACE lp/gurobi_solver_interface.h lp/gurobi_solver_interface.cc) + endif() + endif() create_fast_downward_library( diff --git a/src/search/cmake/FindGurobi.cmake b/src/search/cmake/FindGurobi.cmake new file mode 100644 index 0000000000..e0ed0343e0 --- /dev/null +++ b/src/search/cmake/FindGurobi.cmake @@ -0,0 +1,43 @@ +# Find Gurobi and export the target gurobi::gurobi +# +# Usage: +# find_package(Gurobi) +# target_link_libraries( PRIVATE gurobi::gurobi) +# +# Hints: +# -DGurobi_ROOT=... +# -Dgurobi_DIR=... +# env GUROBI_HOME=... (preferred) + +set(HINT_PATHS ${Gurobi_ROOT} ${gurobi_DIR} $ENV{GUROBI_HOME}) + +find_path(GUROBI_INCLUDE_DIR + NAMES gurobi_c.h gurobi_c++.h + HINTS ${HINT_PATHS} + PATH_SUFFIXES include +) + +# For linux. +find_library(GUROBI_LIBRARY + NAMES + gurobi130 # Gurobi 13.0 + HINTS ${HINT_PATHS} + PATH_SUFFIXES lib +) + +# Check if everything was found and set Gurobi_FOUND. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Gurobi + REQUIRED_VARS GUROBI_INCLUDE_DIR GUROBI_LIBRARY +) + +if(Gurobi_FOUND AND NOT TARGET gurobi::gurobi) + add_library(gurobi::gurobi UNKNOWN IMPORTED) + set_target_properties(gurobi::gurobi PROPERTIES + IMPORTED_LOCATION "${GUROBI_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${GUROBI_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(GUROBI_INCLUDE_DIR GUROBI_LIBRARY) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc new file mode 100644 index 0000000000..bae4592e6d --- /dev/null +++ b/src/search/lp/gurobi_solver_interface.cc @@ -0,0 +1,415 @@ +#include "gurobi_solver_interface.h" + +#include "lp_solver.h" + +#include "../utils/system.h" + +#include +#include +#include +#include + +using namespace std; + +namespace lp { + +namespace { + +NO_RETURN void handle_gurobi_error(GRBenv *env, int error_code) { + if (error_code == GRB_ERROR_OUT_OF_MEMORY) { + utils::exit_with(utils::ExitCode::SEARCH_OUT_OF_MEMORY); + } + + const char *message = env ? GRBgeterrormsg(env) : nullptr; + if (message) { + cerr << "Gurobi error: " << message << endl; + } else { + cerr << "Gurobi error." << endl; + } + cerr << "Gurobi error code: " << error_code << endl; + + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); +} + +template +void GRB_CALL(GRBenv *env, Function function, Args &&...args) { + int status = function(forward(args)...); + if (status) { + handle_gurobi_error(env, status); + } +} + +int objective_sense_to_gurobi(LPObjectiveSense sense) { + switch (sense) { + case LPObjectiveSense::MINIMIZE: + return GRB_MINIMIZE; + case LPObjectiveSense::MAXIMIZE: + return GRB_MAXIMIZE; + } + + ABORT("Unknown LP objective sense."); +} + +char constraint_sense_to_gurobi(LPConstraintSense sense) { + switch (sense) { + case LPConstraintSense::GREATER_EQUAL: + return GRB_GREATER_EQUAL; + case LPConstraintSense::LESS_EQUAL: + return GRB_LESS_EQUAL; + case LPConstraintSense::EQUAL: + return GRB_EQUAL; + } + + ABORT("Unknown LP constraint sense."); +} + +void add_constraint( + GRBenv *env, GRBmodel *model, const LPConstraint &constraint) { + const vector &indices = constraint.get_variables(); + const vector &coefficients = constraint.get_coefficients(); + + assert(indices.size() == coefficients.size()); + + int num_nonzero = static_cast(indices.size()); + int *index_data = + num_nonzero > 0 ? const_cast(indices.data()) : nullptr; + double *coefficient_data = + num_nonzero > 0 ? const_cast(coefficients.data()) : nullptr; + + GRB_CALL( + env, GRBaddconstr, model, num_nonzero, index_data, coefficient_data, + constraint_sense_to_gurobi(constraint.get_sense()), + constraint.get_right_hand_side(), nullptr); +} + +int get_model_status(GRBenv *env, GRBmodel *model) { + assert(model); + + int status; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + return status; +} + +} // namespace + +GurobiSolverInterface::GurobiSolverInterface() + : env(nullptr), + model(nullptr), + num_permanent_constraints(0), + num_temporary_constraints(0), + has_pending_constraint_additions(false) { + int status = GRBloadenv(&env, ""); + if (status) { + handle_gurobi_error(env, status); + } + + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_OUTPUTFLAG, 0); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_LOGTOCONSOLE, 0); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_THREADS, 1); +} + +GurobiSolverInterface::~GurobiSolverInterface() { + if (model) { + GRBfreemodel(model); + } + if (env) { + GRBfreeenv(env); + } +} + +void GurobiSolverInterface::load_problem(const LinearProgram &lp) { + if (model) { + GRBfreemodel(model); + model = nullptr; + } + + const auto &variables = lp.get_variables(); + int num_variables = static_cast(variables.size()); + + vector objective_coefficients; + vector lower_bounds; + vector upper_bounds; + vector variable_types; + + objective_coefficients.reserve(num_variables); + lower_bounds.reserve(num_variables); + upper_bounds.reserve(num_variables); + variable_types.reserve(num_variables); + + for (const LPVariable &variable : variables) { + objective_coefficients.push_back(variable.objective_coefficient); + lower_bounds.push_back(variable.lower_bound); + upper_bounds.push_back(variable.upper_bound); + variable_types.push_back( + variable.is_integer ? GRB_INTEGER : GRB_CONTINUOUS); + } + + double *objective_data = + num_variables > 0 ? objective_coefficients.data() : nullptr; + double *lower_bound_data = + num_variables > 0 ? lower_bounds.data() : nullptr; + double *upper_bound_data = + num_variables > 0 ? upper_bounds.data() : nullptr; + char *variable_type_data = + num_variables > 0 ? variable_types.data() : nullptr; + + GRB_CALL( + env, GRBnewmodel, env, &model, "downward", num_variables, + objective_data, lower_bound_data, upper_bound_data, variable_type_data, + nullptr); + + GRB_CALL( + env, GRBsetintattr, model, GRB_INT_ATTR_MODELSENSE, + objective_sense_to_gurobi(lp.get_sense())); + + const auto &constraints = lp.get_constraints(); + + num_permanent_constraints = static_cast(constraints.size()); + num_temporary_constraints = 0; + + for (const LPConstraint &constraint : constraints) { + add_constraint(env, model, constraint); + } + + has_pending_constraint_additions = constraints.size() > 0; +} + +void GurobiSolverInterface::add_temporary_constraints( + const named_vector::NamedVector &constraints) { + assert(model); + + for (const LPConstraint &constraint : constraints) { + add_constraint(env, model, constraint); + } + + num_temporary_constraints += static_cast(constraints.size()); + if (constraints.size() > 0) { + has_pending_constraint_additions = true; + } +} + +void GurobiSolverInterface::clear_temporary_constraints() { + assert(model); + + if (!has_temporary_constraints()) { + return; + } + + if (has_pending_constraint_additions) { + GRB_CALL(env, GRBupdatemodel, model); + has_pending_constraint_additions = false; + } + + vector indices(num_temporary_constraints); + iota(indices.begin(), indices.end(), num_permanent_constraints); + + GRB_CALL( + env, GRBdelconstrs, model, num_temporary_constraints, indices.data()); + + num_temporary_constraints = 0; + has_pending_constraint_additions = false; +} + +double GurobiSolverInterface::get_infinity() const { + return GRB_INFINITY; +} + +void GurobiSolverInterface::set_objective_coefficients( + const vector &coefficients) { + assert(model); + assert(coefficients.size() == static_cast(get_num_variables())); + + if (coefficients.empty()) { + return; + } + + GRB_CALL( + env, GRBsetdblattrarray, model, GRB_DBL_ATTR_OBJ, 0, + static_cast(coefficients.size()), + const_cast(coefficients.data())); +} + +void GurobiSolverInterface::set_objective_coefficient( + int index, double coefficient) { + assert(model); + assert(index >= 0 && index < get_num_variables()); + + GRB_CALL( + env, GRBsetdblattrelement, model, GRB_DBL_ATTR_OBJ, index, coefficient); +} + +void GurobiSolverInterface::set_constraint_rhs( + int index, double right_hand_side) { + assert(model); + assert(index >= 0 && index < get_num_constraints()); + + if (has_pending_constraint_additions) { + GRB_CALL(env, GRBupdatemodel, model); + has_pending_constraint_additions = false; + } + + GRB_CALL( + env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, + right_hand_side); +} + +void GurobiSolverInterface::set_constraint_sense( + int index, LPConstraintSense sense) { + assert(model); + assert(index >= 0 && index < get_num_constraints()); + + if (has_pending_constraint_additions) { + GRB_CALL(env, GRBupdatemodel, model); + has_pending_constraint_additions = false; + } + + GRB_CALL( + env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, + constraint_sense_to_gurobi(sense)); +} + +void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { + assert(model); + assert(index >= 0 && index < get_num_variables()); + + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_LB, index, bound); +} + +void GurobiSolverInterface::set_variable_upper_bound(int index, double bound) { + assert(model); + assert(index >= 0 && index < get_num_variables()); + + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_UB, index, bound); +} + +void GurobiSolverInterface::set_mip_gap(double gap) { + assert(gap >= 0.0); + + GRB_CALL(env, GRBsetdblparam, env, GRB_DBL_PAR_MIPGAP, gap); + + if (model) { + GRBenv *model_env = GRBgetenv(model); + GRB_CALL(model_env, GRBsetdblparam, model_env, GRB_DBL_PAR_MIPGAP, gap); + } +} + +void GurobiSolverInterface::solve() { + assert(model); + + GRB_CALL(env, GRBoptimize, model); + has_pending_constraint_additions = false; +} + +void GurobiSolverInterface::write_lp(const string &filename) const { + assert(model); + + GRB_CALL(env, GRBwrite, model, filename.c_str()); + has_pending_constraint_additions = false; +} + +void GurobiSolverInterface::print_failure_analysis() const { + assert(model); + + int status = get_model_status(env, model); + + int solution_count; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_SOLCOUNT, &solution_count); + + cerr << "Gurobi optimization failed with status " << status << " and " + << solution_count << " stored solution(s)." << endl; + + switch (status) { + case GRB_INFEASIBLE: + cerr << "The model is infeasible." << endl; + break; + case GRB_UNBOUNDED: + cerr << "The model is unbounded." << endl; + break; + case GRB_INF_OR_UNBD: + cerr << "The model is infeasible or unbounded." << endl; + break; + case GRB_TIME_LIMIT: + cerr << "The time limit was reached." << endl; + break; + case GRB_ITERATION_LIMIT: + cerr << "The iteration limit was reached." << endl; + break; + case GRB_NUMERIC: + cerr << "Gurobi encountered numerical difficulties." << endl; + break; + case GRB_SUBOPTIMAL: + cerr << "Gurobi found a suboptimal solution." << endl; + break; + case GRB_INTERRUPTED: + cerr << "The optimization was interrupted." << endl; + break; + default: + break; + } +} + +bool GurobiSolverInterface::is_infeasible() const { + return get_model_status(env, model) == GRB_INFEASIBLE; +} + +bool GurobiSolverInterface::is_unbounded() const { + return get_model_status(env, model) == GRB_UNBOUNDED; +} + +bool GurobiSolverInterface::has_optimal_solution() const { + return get_model_status(env, model) == GRB_OPTIMAL; +} + +double GurobiSolverInterface::get_objective_value() const { + assert(model); + assert(has_optimal_solution()); + + double objective_value; + GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_OBJVAL, &objective_value); + + return objective_value; +} + +vector GurobiSolverInterface::extract_solution() const { + assert(model); + assert(has_optimal_solution()); + + int num_variables = get_num_variables(); + vector solution(num_variables); + + if (num_variables > 0) { + GRB_CALL( + env, GRBgetdblattrarray, model, GRB_DBL_ATTR_X, 0, num_variables, + solution.data()); + } + + return solution; +} + +int GurobiSolverInterface::get_num_variables() const { + assert(model); + + int num_variables; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_NUMVARS, &num_variables); + + return num_variables; +} + +int GurobiSolverInterface::get_num_constraints() const { + return num_permanent_constraints + num_temporary_constraints; +} + +bool GurobiSolverInterface::has_temporary_constraints() const { + return num_temporary_constraints > 0; +} + +void GurobiSolverInterface::print_statistics() const { + assert(model); + + double runtime; + GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_RUNTIME, &runtime); + + cout << "Gurobi runtime: " << runtime << "s" << endl; +} + +} // namespace lp diff --git a/src/search/lp/gurobi_solver_interface.h b/src/search/lp/gurobi_solver_interface.h new file mode 100644 index 0000000000..fc03fa9548 --- /dev/null +++ b/src/search/lp/gurobi_solver_interface.h @@ -0,0 +1,57 @@ +#ifndef LP_GUROBI_SOLVER_INTERFACE_H +#define LP_GUROBI_SOLVER_INTERFACE_H + +#include "solver_interface.h" + +#include + +namespace lp { +class GurobiSolverInterface : public SolverInterface { + GRBenv *env; + GRBmodel *model; + int num_permanent_constraints; + int num_temporary_constraints; + mutable bool has_pending_constraint_additions; + +public: + GurobiSolverInterface(); + virtual ~GurobiSolverInterface() override; + + virtual void load_problem(const LinearProgram &lp) override; + virtual void add_temporary_constraints( + const named_vector::NamedVector &constraints) override; + virtual void clear_temporary_constraints() override; + virtual double get_infinity() const override; + + virtual void set_objective_coefficients( + const std::vector &coefficients) override; + virtual void set_objective_coefficient( + int index, double coefficient) override; + virtual void set_constraint_rhs(int index, double right_hand_side) override; + virtual void set_constraint_sense( + int index, LPConstraintSense sense) override; + virtual void set_variable_lower_bound(int index, double bound) override; + virtual void set_variable_upper_bound(int index, double bound) override; + + virtual void set_mip_gap(double gap) override; + + virtual void solve() override; + virtual void write_lp(const std::string &filename) const override; + virtual void print_failure_analysis() const override; + virtual bool is_infeasible() const override; + virtual bool is_unbounded() const override; + + virtual bool has_optimal_solution() const override; + + virtual double get_objective_value() const override; + + virtual std::vector extract_solution() const override; + + virtual int get_num_variables() const override; + virtual int get_num_constraints() const override; + virtual bool has_temporary_constraints() const override; + virtual void print_statistics() const override; +}; +} + +#endif diff --git a/src/search/lp/lp_solver.cc b/src/search/lp/lp_solver.cc index b85ff40cd6..ac85830468 100644 --- a/src/search/lp/lp_solver.cc +++ b/src/search/lp/lp_solver.cc @@ -6,6 +6,9 @@ #ifdef HAS_SOPLEX #include "soplex_solver_interface.h" #endif +#ifdef HAS_GUROBI +#include "gurobi_solver_interface.h" +#endif #include "../plugins/plugin.h" @@ -146,6 +149,13 @@ LPSolver::LPSolver(LPSolverType solver_type) { pimpl = make_unique(); #else missing_solver = "SoPlex"; +#endif + break; + case LPSolverType::GUROBI: +#ifdef HAS_GUROBI + pimpl = make_unique(); +#else + missing_solver = "Gurobi"; #endif break; default: @@ -256,5 +266,6 @@ void LPSolver::print_statistics() const { static plugins::TypedEnumPlugin _enum_plugin( {{"cplex", "commercial solver by IBM"}, - {"soplex", "open source solver by ZIB"}}); + {"soplex", "open source solver by ZIB"}, + {"gurobi", "commercial solver by Gurobi"}}); } diff --git a/src/search/lp/lp_solver.h b/src/search/lp/lp_solver.h index 27b6cac132..78ff7c01b0 100644 --- a/src/search/lp/lp_solver.h +++ b/src/search/lp/lp_solver.h @@ -17,7 +17,8 @@ class Options; namespace lp { enum class LPSolverType { CPLEX, - SOPLEX + SOPLEX, + GUROBI }; enum class LPObjectiveSense { diff --git a/src/search/lp/soplex_solver_interface.cc b/src/search/lp/soplex_solver_interface.cc index e420372666..5d4024743f 100644 --- a/src/search/lp/soplex_solver_interface.cc +++ b/src/search/lp/soplex_solver_interface.cc @@ -6,6 +6,12 @@ #include +// Windows headers define ERROR as a macro, which conflicts with SoPlex's enum +// SPxSolverBase::Status defined in spxsolver.h +#ifdef ERROR +#undef ERROR +#endif + using namespace std; using namespace soplex;